动态规划
- 1.基本动态规划 一维
- (1)爬楼梯(70)
- (2)打家劫舍(198)
- (3)等差数列划分(413)
- 2.基本动态规划 二维
- (1)最小路径和(64)
- (2)01 矩阵(542)
- (3)最大正方形(221)
- 3.分割类型题
- (1) 完全平方数(279)
- (2)解码方法
- (3)单词拆分(139)
- 4.子序列问题
- (1)最长递增子序列(300)
- (2)最长公共子序列(1143)
- 5.背包问题
- (1)分割等和子集(416)
- (2)一和零(474)
- (3)零钱兑换(322)
- 6.字符串编辑
- (1)编辑距离(72)
- (2)只有两个键的键盘(650)
- (3)正则表达式匹配(10)
- 7.股票交易
- (1)买卖股票的最佳时机(121)
- (2)买卖股票的最佳时机 II(122)
- (3)买卖股票的最佳时机 III(123)
- (4)买卖股票的最佳时机 IV
- (5)最佳买卖股票时机含冷冻期(309)
- (6) 买卖股票的最佳时机含手续费(714)
- 8.练习
- (1)打家劫舍 II(213)
- (2)最大子数组和(53)
- (3)整数拆分(343)
- (4)两个字符串的删除操作(583)
- (5)最长数对链(646)
- (6)摆动序列(376)
- (7)目标和(494)
1.基本动态规划 一维
(1)爬楼梯(70)
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶
输入:n = 3
输出:3
解释:有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶
由题意可得出,f(n)=f(n-1)+f(n-2)
//递归方法
public class test {
public static void main(String[] args) {
int n=4;
test t=new test();
System.out.println(t.climbStairs(n));
}
public int climbStairs(int n) {
if (n==1){
return 1;
}
if (n==2){
return 2;
}
return climbStairs(n-1)+climbStairs(n-2);
}
}
//记忆化搜索方法 将已经计算过的楼梯的方法记录下来,避免重复计算
public class test {
public static void main(String[] args) {
int n=4;
test t=new test();
System.out.println(t.climbStairs(n));
}
public int climbStairs(int n) {
int mem[]=new int[n+1];
return recursion(mem,n);
}
private int recursion(int[] mem, int n) {
//如果到达第n层楼梯的方法已经计算过了,就直接返回,不需要重复计算
if (mem[n]>0){
return mem[n];
}
if (n==1){
mem[n]=1;
}else if (n==2){
mem[n]=2;
}else {
mem[n]=recursion(mem,n-1)+recursion(mem,n-2);
}
//返回方法数
return mem[n];
}
}
//动态规划
public class test {
public static void main(String[] args) {
int n=4;
test t=new test();
System.out.println(t.climbStairs(n));
}
public int climbStairs(int n) {
if (n==1){
return 1;
}
int[] dp=new int[n+1];
dp[1]=1;
dp[2]=2;
for (int i=3;i<=n;i++){
dp[i]=dp[i-1]+dp[i-2];
}
return dp[n];
}
}
//滚动数组思想
public class test {
public static void main(String[] args) {
int n=4;
test t=new test();
System.out.println(t.climbStairs(n));
}
public int climbStairs(int n) {
int one=0,two=0,r=1;
//交换 f(x)=f(x-1)+f(x-2) 每一次交换f(x-2)=f(x-1) f(x-1)=f(x)位置,这样可以保证不开辟新的空间
//就可以得出最后的f(n)
for (int i=1;i<=n;i++){
one=two;
two=r;
r=one+two;
}
return r;
}
}
(2)打家劫舍(198)
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。
当来到第i间房时,我们有两种选择:
(1)偷第i间房,则偷窃总金额就是 nums[i]+偷窃前i-2间房屋的最大总金额
(2)不偷第i间房,则偷窃总金额为偷窃前i-1间房的最高总金额
则 最高总金额 = max( (1) , (2) )
//动态规划
public class test {
public static void main(String[] args) {
int[] arr={1,2,3,1};
test t=new test();
System.out.println(t.rob(arr));
}
public int rob(int[] nums) {
if (nums==null||nums.length<1){
return 0;
}
//如果只有一个房间,就返回这个房间的金额,这样是为了避免后边给dp[1]赋值的时候报错
if (nums.length==1){
return nums[0];
}
int[] dp=new int[nums.length];
dp[0]=nums[0];
dp[1]=Math.max(nums[0],nums[1]);
for (int i=2;i< nums.length;i++){
dp[i]=Math.max(dp[i-2]+nums[i],dp[i-1]);
}
return dp[nums.length-1];
}
}
//滑动数组
public class test {
public static void main(String[] args) {
int[] arr={1,2,3,1};
test t=new test();
System.out.println(t.rob(arr));
}
public int rob(int[] nums) {
if (nums==null||nums.length<1){
return 0;
}
//如果只有一个房间,就返回这个房间的金额,这样是为了避免后边给第二间房赋值的时候报错
if (nums.length==1){
return nums[0];
}
int first=nums[0];
int second=Math.max(nums[0],nums[1]);
for (int i=2;i< nums.length;i++){
int temp=second;
second=Math.max(first+nums[i],second);
first=temp;
}
return second;
}
}
(3)等差数列划分(413)
如果一个数列 至少有三个元素 ,并且任意两个相邻元素之差相同,则称该数列为等差数列。
例如,[1,3,5,7,9]、[7,7,7,7] 和 [3,-1,-5,-9] 都是等差数列。
给你一个整数数组 nums ,返回数组 nums 中所有为等差数组的 子数组 个数。
子数组 是数组中的一个连续序列。
输入:nums = [1,2,3,4]
输出:3
解释:nums 中有三个子等差数组:[1, 2, 3]、[2, 3, 4] 和 [1,2,3,4] 自身。
输入:nums = [1]
输出:0
public class test {
public static void main(String[] args) {
int[] arr={1,2,3,4,5};
test t=new test();
System.out.println(t.numberOfArithmeticSlices(arr));
}
public int numberOfArithmeticSlices(int[] nums) {
//如果数组中元素不足3个直接返回0
if (nums.length<=2){
return 0;
}
int d=nums[0]-nums[1];
int t=0,ans=0;
for (int i=2;i<nums.length;i++){
if (nums[i-1]-nums[i]==d){
t++;
}else {
d=nums[i-1]-nums[i];
t=0;
}
ans+=t;
}
return ans;
}
}
2.基本动态规划 二维
(1)最小路径和(64)
给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。
由于路径的方向只能是向下或者向右
因此网格的第一行的每个元素只能从左上角元素开始向右移动到达:dp[0][j]=dp[0][j-1]+grid[0][j]
网格第一列的每个元素只能从左上角元素开始向下移动到达:dp[i][0]=dp[i-1][0]+grid[i][0]
网格其他位置 dp[i][j]=grid[i][j]+min(dp[i-1][j],dp[i][j-1])
这样一直遍历给dp数组赋值,最后得到的dp[m-1][n-1]的值即为从网格左上角到网格右下角的值
public class test {
public static void main(String[] args) {
int[][] arr={{1,3,1},{1,5,1},{4,2,1}};
test t=new test();
System.out.println(t.minPathSum(arr));
}
public int minPathSum(int[][] grid) {
if (grid==null||grid.length<1){
return 0;
}
int m= grid.length;
int n=grid[0].length;
int[][] dp=new int[m][n];
dp[0][0]=grid[0][0];//左上角
//给第一行网格赋值
for (int j=1;j<n;j++){
dp[0][j]=dp[0][j-1]+grid[0][j];
}
//给第一列网格赋值
for (int i=1;i<m;i++){
dp[i][0]=dp[i-1][0]+grid[i][0];
}
//给其余网格赋值
for (int i=1;i<m;i++){
for (int j=1;j<n;j++){
dp[i][j]=grid[i][j]+Math.min(dp[i-1][j],dp[i][j-1]);
}
}
//dp[m-1][n-1]就是从左上角到右下角的最短路径
return dp[m-1][n-1];
}
}
(2)01 矩阵(542)
给定一个由 0 和 1 组成的矩阵 mat ,请输出一个大小相同的矩阵,其中每一个格子是 mat 中对应位置元素到最近的 0 的距离。
两个相邻元素间的距离为 1 。
输入:mat = [[0,0,0],[0,1,0],[0,0,0]]
输出:[[0,0,0],[0,1,0],[0,0,0]]
输入:mat = [[0,0,0],[0,1,0],[1,1,1]]
输出:[[0,0,0],[0,1,0],[1,2,1]]
(1)广度优先遍历:将所有的0都加入队列,然后从0开始扩散,找到1距离0的距离
(2)动态规划:从四个方向依次遍历某个1到0的最短路径,得到四个f(i,j),取最小的。
也可以只需要从左上到右下,再从右下到左上遍历,得到两个f(i,j),取最小的
//广度优先遍历
public class test {
public static void main(String[] args) {
int[][] arr={{0,0,0},{0,1,0},{0,0,0}};
test t=new test();
int[][] matrix = t.updateMatrix(arr);
for (int i=0;i< matrix.length;i++){
for (int j=0;j<matrix[0].length;j++){
System.out.print(matrix[i][j]+" ");
}
System.out.println();
}
}
public int[][] updateMatrix(int[][] mat) {
int m= mat.length;
int n=mat[0].length;
//定义方向数组
int[][] dirs={{-1,0},{1,0},{0,-1},{0,1}};
//定义距离数组
int[][] dist=new int[m][n];
//定义一个boolean类型的数组,来存储当前元素是否已经求过距离
boolean[][] used=new boolean[m][n];
//定义一个队列,用来存储0和已经求过距离的格子的信息
Queue<int[]> queue=new LinkedList<>();
//存储0的格子的信息
for (int i=0;i<m;i++){
for (int j=0;j<n;j++){
if (mat[i][j]==0){
//加入队列
queue.add(new int[]{i,j});
used[i][j]=true;
}
}
}
//广度优先遍历:从队列中取出0,从0扩散
while (!queue.isEmpty()){
//弹出queue里的元素
int[] poll = queue.poll();
int i=poll[0];
int j=poll[1];
//从四个方向扩散,求距离
for (int[] dir:dirs){
//被扩散的元素的坐标
int newI=i+dir[0];
int newJ=j+dir[1];
//如果满足条件
if (newI>=0&&newI<m&&newJ>=0&&newJ<n&&!used[newI][newJ]){
//新扩散的元素的距离,为当前元素的距离+1
dist[newI][newJ]=1+dist[i][j];
//将新扩散的元素的位置信息加入队列
queue.add(new int[]{newI,newJ});
//标记 证明已经扩散过了
used[newI][newJ]=true;
}
}
}
return dist;
}
}
//动态规划
public class test {
public static void main(String[] args) {
int[][] arr={{0,0,0},{0,1,0},{0,0,0}};
test t=new test();
int[][] matrix = t.updateMatrix(arr);
for (int i=0;i< matrix.length;i++){
for (int j=0;j<matrix[0].length;j++){
System.out.print(matrix[i][j]+" ");
}
System.out.println();
}
}
public int[][] updateMatrix(int[][] mat) {
int m= mat.length;
int n=mat[0].length;
//定义距离数组
int[][] dist=new int[m][n];
//初始化动态规划的数组 如果元素为0则距离为0 如果元素为1则距离初始设置为很大的数
for (int i=0;i<m;i++){
for (int j=0;j<n;j++){
if (mat[i][j]==0){
dist[i][j]=0;
}else {
dist[i][j]=Integer.MAX_VALUE/10;
}
}
}
//从左上到右下遍历数组 只需要考虑下边和右边的影响
for (int i=m-1;i>=0;i--){
for (int j=n-1;j>=0;j--){
if (i+1<m){
dist[i][j]=Math.min(dist[i][j],dist[i+1][j]+1);
}
if (j+1<n){
dist[i][j]=Math.min(dist[i][j],dist[i][j+1]+1);
}
}
}
//从右下到左上遍历数组 只需要考虑上边和左边的影响
for (int i=0;i<m;i++){
for (int j=0;j<n;j++){
if (i-1>=0){
dist[i][j]=Math.min(dist[i][j],dist[i-1][j]+1);
}
if (j-1>=0){
dist[i][j]=Math.min(dist[i][j],dist[i][j-1]+1);
}
}
}
return dist;
}
}
(3)最大正方形(221)
在一个由 ‘0’ 和 ‘1’ 组成的二维矩阵内,找到只包含 ‘1’ 的最大正方形,并返回其面积。
输入:matrix = [["1","0","1","0","0"],["1","0","1","1","1"],["1","1","1","1","1"],["1","0","0","1","0"]]
输出:4
输入:matrix = [["0","1"],["1","0"]]
输出:1
(1)暴力法:依次遍历数组中每个元素,如果这个元素为1就以这个元素找最大可能的正方形,最后最大的那个值就是答案
(2)递归:如果该位置的值是0,则dp[i][j]=0,如果该位置值是1,dp[i][j]的值是其上方、左方、左上方的三个相邻位置的
dp值的最小值+1
//暴力遍历
public class test {
public static void main(String[] args) {
char[][] arr={{'1','0','1','0','0'},
{'1','0','1','1','1'},
{'1','1','1','1','1'},
{'1','0','0','1','0'}};
test t=new test();
int maxSquare=t.maximalSquare(arr);
System.out.println(maxSquare);
}
public int maximalSquare(char[][] matrix) {
int maxSize=0;//最大正方形的面积
int maxSide=0;//正方形的边长
int m=matrix.length;
int n=matrix[0].length;
if (matrix==null||m<1||n<1){
return maxSize;
}
//遍历整个数组
for (int i=0;i<m;i++){
for (int j=0;j<n;j++){
//遇到一个1 作为正方形的左上角
if (matrix[i][j]=='1'){
//当前的正方形边长与1两者之间较大的值
maxSide=Math.max(maxSide,1);
//计算可能的最大正方形边长
int maybeMaxSide=Math.min(m-i,n-j);
//从当前1下一个格子开始遍历直到可能的最大正方形边长
for (int k=1;k<maybeMaxSide;k++){
//判断新增的一行一列是否均为1
//标志 是否可以成一个正方形
boolean flag=true;
if (matrix[i+k][j+k]=='0'){
break;
}
for (int f=0;f<k;f++){
if (matrix[i+k][j+f]=='0'||matrix[i+f][j+k]=='0'){
flag=false;
break;
}
}
if (flag){
maxSide=Math.max(maxSide,k+1);
}else {
break;
}
}
}
}
}
maxSize=maxSide*maxSide;
return maxSize;
}
}
//递归
public class test {
public static void main(String[] args) {
char[][] arr={{'1','0','1','0','0'},
{'1','0','1','1','1'},
{'1','1','1','1','1'},
{'1','0','0','1','0'}};
test t=new test();
int maxSquare=t.maximalSquare(arr);
System.out.println(maxSquare);
}
public int maximalSquare(char[][] matrix) {
int maxSize=0;//最大正方形的面积
int maxSide=0;//正方形的边长
int m=matrix.length;
int n=matrix[0].length;
if (matrix==null||m<1||n<1){
return maxSize;
}
int[][] dp=new int[m][n];
for (int i=0;i<m;i++){
for (int j=0;j<n;j++){
if (matrix[i][j]=='1'){
//边界条件 如果i j至少有一个为0 则dp[i][j]只能为1
if (i==0||j==0){
dp[i][j]=1;
}else {
dp[i][j]=Math.min(Math.min(dp[i-1][j],dp[i][j-1]),dp[i-1][j-1])+1;
}
maxSide=Math.max(maxSide,dp[i][j]);
}
}
}
maxSize=maxSide*maxSide;
return maxSize;
}
}
3.分割类型题
(1) 完全平方数(279)
给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4
输入:n = 13
输出:2
解释:13 = 4 + 9
//动态规划
public class test {
public static void main(String[] args) {
int n=12;
test t=new test();
System.out.println(t.numSquares(n));
}
public int numSquares(int n) {
int[] dp=new int[n+1];
for (int i=1;i<=n;i++){
int minNum=Integer.MAX_VALUE;
for (int j=1;j*j<=i;j++){
minNum=Math.min(minNum,dp[i-j*j]);
}
dp[i]=minNum+1;
}
return dp[n];
}
}
//数学方法 看原题题解
public class test {
public static void main(String[] args) {
int n=12;
test t=new test();
System.out.println(t.numSquares(n));
}
public int numSquares(int n) {
if (isPerfectSquare(n)){
return 1;
}
if (checkAnswer4(n)){
return 4;
}
for (int i=1;i*i<=n;i++){
int j=n-i*i;
if (isPerfectSquare(j)){
return 2;
}
}
return 3;
}
//判断是否为完全平方数
public boolean isPerfectSquare(int x){
int y=(int)Math.sqrt(x);
return y*y==x;
}
//判断是否能表示为 4^k*(8m+7)
public boolean checkAnswer4(int x){
while (x%4==0){
x/=4;
}
return x%8==7;
}
}
(2)解码方法
一条包含字母 A-Z 的消息通过以下映射进行了 编码 :
‘A’ -> “1”
‘B’ -> “2”
…
‘Z’ -> “26”
要 解码 已编码的消息,所有数字必须基于上述映射的方法,反向映射回字母(可能有多种方法)。例如,“11106” 可以映射为:
“AAJF” ,将消息分组为 (1 1 10 6)
“KJF” ,将消息分组为 (11 10 6)
注意,消息不能分组为 (1 11 06) ,因为 “06” 不能映射为 “F” ,这是由于 “6” 和 “06” 在映射中并不等价。
给你一个只含数字的 非空 字符串 s ,请计算并返回 解码 方法的 总数 。
题目数据保证答案肯定是一个 32 位 的整数。
输入:s = "12"
输出:2
解释:它可以解码为 "AB"(1 2)或者 "L"(12)。
输入:s = "226"
输出:3
解释:它可以解码为 "BZ" (2 26), "VF" (22 6), 或者 "BBF" (2 2 6) 。
输入:s = "06"
输出:0
解释:"06" 无法映射到 "F" ,因为存在前导零("6" 和 "06" 并不等价)。
public class test {
public static void main(String[] args) {
String s="12";
test t=new test();
System.out.println(t.numDecodings(s));
}
public int numDecodings(String s) {
//将字符串转为字符数组
char[] chars = s.toCharArray();
int n= chars.length;
int[] dp=new int[n+1];
if (chars[0]=='0'){
return 0;
}
//动态规划边界条件 空字符串也有一种解法
dp[0]=1;
for (int i=1;i<=n;i++){
if (chars[i-1]!='0'){
dp[i]+=dp[i-1];
}
if (i>1&&chars[i-2]!='0'&&((chars[i-2]-'0')*10+(chars[i-1]-'0'))<=26){
dp[i]+=dp[i-2];
}
}
return dp[n];
}
}
(3)单词拆分(139)
给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。
输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。
注意,你可以重复使用字典中的单词。
输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
输出: false
这个题可以从字符串s的第一个字符开始判断,判断第一个字符到第j字符是否在字典中存在,如果存在则令dp[j]=true表示当前
位置可以作为一个合法的分割点,下一次寻找单词必须从分割点开始。
public class test {
public static void main(String[] args) {
String s="leetcode";
List<String> wordDict=new ArrayList<>();
wordDict.add("leet");
wordDict.add("code");
System.out.println(wordBreak(s,wordDict));
}
public static boolean wordBreak(String s, List<String> wordDict) {
//将字典放入set集合,这样可以先将wordDict中可能出现的重复单词去重,
//并且方便判断s的某个序列是否在字典中
HashSet<String> set=new HashSet<>(wordDict);
//动态规划数组 dp[s.length]表示是否可以用字典拼出s
boolean[] dp=new boolean[s.length()+1];
//动态规划边界条件 从0开始是一个合法的分割点
dp[0]=true;
//依次遍历s的每个字符,判断从某个合法分割点开始到i位置 能否在字典中找出单词
for (int i=1;i<=s.length();i++){
//从0开始 找从某个合法的分割点到i能否在字典找到对应的单词
for (int j=0;j<i;j++){
if (dp[j]&&set.contains(s.substring(j,i))){
dp[i]=true;//表示i位置是一个合法的分割点
break;//退出本次寻找
}
}
}
return dp[s.length()];
}
}
4.子序列问题
(1)最长递增子序列(300)
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
输入:nums = [0,1,0,3,2,3]
输出:4
输入:nums = [7,7,7,7,7,7,7]
输出:1
动态规划:当来到i位置时,从i-1位置开始遍历直到0位置,找到小于nums[i]的并且dp值最大的那个位置假设为j,
dp[i]=dp[j]+1
public class test {
public static void main(String[] args) {
int[] nums={10,9,2,5,3,7,101,18};
System.out.println(lengthOfLIS(nums));
}
public static int lengthOfLIS(int[] nums) {
int longest=0;//最长严格递增子序列的长度
if (nums==null||nums.length<1){
return 0;
}
//dp 用来保存以每一个数字结尾的最长严格递增子序列的长度
int[] dp=new int[nums.length+1];
//每个数字本身就是一个严格递增子序列
for (int i=0;i< nums.length;i++){
dp[i]=1;
}
//遍历数组中的每一个数字
for (int i=1;i< nums.length;i++){
int curLong=0;//以某个位置结尾的最长严格递增子序列的长度
for (int j=i-1;j>=0;j--){
//找到小于nums[i]的位置
if (nums[i]>nums[j]){
curLong=Math.max(curLong,dp[j]);
}
}
dp[i]=curLong+1;
}
for (int i=0;i< nums.length;i++){
longest=Math.max(longest,dp[i]);
}
return longest;
}
}
(2)最长公共子序列(1143)
给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace" ,它的长度为 3 。
输入:text1 = "abc", text2 = "def"
输出:0
解释:两个字符串没有公共子序列,返回 0 。
因为需要比较两个字符串,所以我们可以想到使用二维dp数组来解决问题。当某个字符串是空串时,那么它和另一字符串的最长公共
子序列的长度都是0,即dp[0][j]=0,dp[i][0]=0,即当i=0或j=0时,dp[i][j]=0。当i>0且j>0时,当i位置的数字和j位置的
数字相同时,dp[i][j]=dp[i-1][j-1]+1。当不相同时,dp[i][j]=max(dp[i-1][j],dp[i][j-1])
public class test {
public static void main(String[] args) {
String text1="abcde";
String text2="ace";
System.out.println(longestCommonSubsequence(text1,text2));
}
public static int longestCommonSubsequence(String text1, String text2) {
int m=text1.length();
int n=text2.length();
char[] ch1=text1.toCharArray();
char[] ch2=text2.toCharArray();
//dp[m][n]表示最长公共子序列
int[][] dp=new int[m+1][n+1];
//从1开始是为了维护i-1时不越界
for (int i=1;i<=m;i++){
for (int j=1;j<=n;j++){
//如果两个字符相等
if (ch1[i-1]==ch2[j-1]){
dp[i][j]=dp[i-1][j-1]+1;
}else {//如果不相等
dp[i][j]=Math.max(dp[i-1][j],dp[i][j-1]);
}
}
}
return dp[m][n];
}
}
5.背包问题
背包问题是一种组合优化的NP完全问题:有N个物品和容量为W的背包,每个物品都有自己的体积w和价值v,求拿哪些物品可以使得
背包所装下物品的总价值最大。如果限定每种物品只能选择0个或1个,则问题称为0-1背包问题;如果不限定每种物品的数量,则问题
称为无界背包问题或完全背包问题。
(1)分割等和子集(416)
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。
题目要求需要将数组分割成两个子集并且使得两个子集的元素和相等,那么就需要数组元素的和为偶数。然后需要使两个子集的元素
和相等,等价于在数组中是否找到元素可以凑成元素和的一半。这就可以看成0-1背包问题,从数组中选取元素,看能否组成数组元素
和的一半。
public class test {
public static void main(String[] args) {
int[] arr={1,5,11,5};
System.out.println(canPartition(arr));
}
public static boolean canPartition(int[] nums) {
int n= nums.length;
//如果数组长度小于2 返回false
if (n<2){
return false;
}
int sum=0;//数组元素和
int maxNum=0;//数组中最大的元素
for (int i=0;i< n;i++){
sum+=nums[i];
maxNum=Math.max(maxNum,nums[i]);
}
//如果数组元素和为奇数返回false
if (sum%2!=0){
return false;
}
int target=sum/2;//目标数
//如果数组中最大的元素大于target 就返回false
if (maxNum>target){
return false;
}
//dp[n-1][target]表示是否可以分割为两个元素和相等的子集
boolean[][] dp=new boolean[n+1][target+1];
//当需要组成0时 一个元素都不选可以组成
for (int i=0;i<n;i++){
dp[i][0]=true;
}
//当i==0时 只有数组第一个数可以选择
dp[0][nums[0]]=true;
//依次遍历数组中的每一个数
for (int i=1;i<n;i++){
//组成从1到target
for (int j=1;j<=target;j++){
if (j>=nums[i]){//如果剩余的目标数大小大于当前元素大小 可以选择或者不选择当前元素
dp[i][j]=dp[i-1][j] | dp[i-1][j-nums[i]];
}else {
dp[i][j]=dp[i-1][j];
}
}
}
return dp[n-1][target];
}
}
//压缩空间
public class test {
public static void main(String[] args) {
int[] arr={1,5,11,5};
System.out.println(canPartition(arr));
}
public static boolean canPartition(int[] nums) {
int n= nums.length;
//如果数组长度小于2 返回false
if (n<2){
return false;
}
int sum=0;//数组元素和
int maxNum=0;//数组中最大的元素
for (int i=0;i< n;i++){
sum+=nums[i];
maxNum=Math.max(maxNum,nums[i]);
}
//如果数组元素和为奇数返回false
if (sum%2!=0){
return false;
}
int target=sum/2;//目标数
//如果数组中最大的元素大于target 就返回false
if (maxNum>target){
return false;
}
//dp[target]表示是否可以分割为两个元素和相等的子集
boolean[] dp=new boolean[target+1];
//当需要组成0时 一个元素都不选可以组成
dp[0]=true;
//依次遍历数组中的每一个数
for (int i=1;i<n;i++){
//组成从target到nums[i] 小于nums[i]的位置显然不能为true
for (int j=target;j>=nums[i];j--){
dp[j]|=dp[j-nums[i]];
}
}
return dp[target];
}
}
(2)一和零(474)
给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。
如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。
输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
输出:4
解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。
其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,
大于 n 的值 3 。
输入:strs = ["10", "0", "1"], m = 1, n = 1
输出:2
解释:最大的子集是 {"0", "1"} ,所以答案是 2 。
本题有0和1两种容量,因此需要使用三维动态规划求解,三个维度分别是字符串、0的容量、1的容量。
定义三维数组dp,其中dp[i][j][k]表示在前i个字符串中,使用j个0和k个1的情况下最多可以得到的字符串数量。
当没有任何字符串可以使用时,可以得到的字符串数量只能是0,因此当i=0时,dp[i][j][k]=0。
当1<=i<=l时,对于strs中的第i个字符串,首先遍历该字符串得到其中的0和1的数量,分别记为zeros和ones,然后对于
0<=j<=m,0<=k<=n,计算dp[i][j][k]的值。
当0和1的容量分别是j和k时,考虑以下两种情况:
如果j<zeros或k<ones,则不能选第i个字符串,此时有dp[i][j][k]=dp[i-1][j][k];
如果j>=zeros且k>=ones,则如果不选第i个字符串,有dp[i][j][k]=dp[i-1][j][k],如果选第i个字符串,有
dp[i][j][k]=dp[i-1][j-zeros][k-ones]+1,dp[i][j][k]的值应取上面两项中的最大值
public class test {
public static void main(String[] args) {
String[] arr={"10", "0001", "111001", "1", "0"};
int m=5,n=3;
test t=new test();
System.out.println(t.findMaxForm(arr,m,n));
}
public int findMaxForm(String[] strs, int m, int n) {
int len= strs.length;
if (len==0){
return 0;
}
//dp[len][m][n]表示最大子集的长度
int[][][] dp=new int[len+1][m+1][n+1];
//遍历每个字符串
for (int i=1;i<=len;i++){
int zeros=getZeros(strs[i-1]);//1的数量
int ones=getOnes(strs[i-1]);//0的数量
for (int j=0;j<=m;j++){//0
for (int k=0;k<=n;k++){//1
if (j<zeros||k<ones){
dp[i][j][k]=dp[i-1][j][k];
}
if (j>=zeros&&k>=ones){
dp[i][j][k]=Math.max(dp[i-1][j][k],dp[i-1][j-zeros][k-ones] + 1);
}
}
}
}
return dp[len][m][n];
}
//获取字符串中1的数量
private int getOnes(String str) {
char[] chars=str.toCharArray();
int ones=0;
for (int i=0;i<str.length();i++){
if (chars[i]=='1'){
ones++;
}
}
return ones;
}
//获取字符串中0的数量
private int getZeros(String str) {
char[] chars=str.toCharArray();
int zeros=0;//字符串中1的数量
for (int i=0;i<str.length();i++){
if (chars[i]=='0'){
zeros++;
}
}
return zeros;
}
}
//压缩空间
public class test {
public static void main(String[] args) {
String[] arr={"10", "0001", "111001", "1", "0"};
int m=5,n=3;
test t=new test();
System.out.println(t.findMaxForm(arr,m,n));
}
public int findMaxForm(String[] strs, int m, int n) {
int len= strs.length;
if (len==0){
return 0;
}
//dp[m][n]表示最大子集的长度
int[][] dp=new int[m+1][n+1];
//遍历每个字符串
for (int i=1;i<=len;i++){
int zeros=getZeros(strs[i-1]);//1的数量
int ones=getOnes(strs[i-1]);//0的数量
for (int j=m;j>=zeros;j--){//0
for (int k=n;k>=ones;k--){//1
dp[j][k]=Math.max(dp[j][k],dp[j-zeros][k-ones] + 1);
}
}
}
return dp[m][n];
}
//获取字符串中1的数量
private int getOnes(String str) {
char[] chars=str.toCharArray();
int ones=0;
for (int i=0;i<str.length();i++){
if (chars[i]=='1'){
ones++;
}
}
return ones;
}
//获取字符串中0的数量
private int getZeros(String str) {
char[] chars=str.toCharArray();
int zeros=0;//字符串中1的数量
for (int i=0;i<str.length();i++){
if (chars[i]=='0'){
zeros++;
}
}
return zeros;
}
}
(3)零钱兑换(322)
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
输入:coins = [2], amount = 3
输出:-1
输入:coins = [1], amount = 0
输出:0
(1)递归 求出每一种可以组成总金额的方法,求其中最少的硬币个数
(2)动态规划
//递归
public class test {
int res = Integer.MAX_VALUE;//所有可以凑成总金额的方式的最少硬币个数
public static void main(String[] args) {
int[] arr={1, 2, 5};
int amount=11;
test t=new test();
System.out.println(t.coinChange(arr,amount));
}
public int coinChange(int[] coins, int amount) {
if (amount==0){
return 0;
}
//递归
findMinCoins(coins,amount,0);
if (res==Integer.MAX_VALUE){
return -1;
}
return res;
}
private void findMinCoins(int[] coins, int amount, int num) {
if (amount<0){
return;
}
if (amount==0){
res=Math.min(num,res);
}
//以每一个硬币开头 递归遍历所有可能的方法
for (int i=0;i<coins.length;i++){
findMinCoins(coins,amount-coins[i],num+1);
}
}
}
//递归
public class test {
public static void main(String[] args) {
int[] arr={2};
int amount=3;
test t=new test();
System.out.println(t.coinChange(arr,amount));
}
public int coinChange(int[] coins, int amount) {
int[] dp=new int[amount+1];
for (int i=0;i<dp.length;i++){
dp[i]=amount+1;
}
dp[0]=0;
for (int i=1;i<=amount;i++){
for (int j=0;j<coins.length;j++){
if (coins[j]<=i){
dp[i]=Math.min(dp[i],dp[i-coins[j]]+1);
}
}
}
return dp[amount]>amount?-1:dp[amount];
}
}
6.字符串编辑
(1)编辑距离(72)
给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符
输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')
输入:word1 = "intention", word2 = "execution"
输出:5
解释:
intention -> inention (删除 't')
inention -> enention (将 'i' 替换为 'e')
enention -> exention (将 'n' 替换为 'x')
exention -> exection (将 'n' 替换为 'c')
exection -> execution (插入 'u')
public class test {
public static void main(String[] args) {
String word1="horse";
String word2="ros";
test t=new test();
System.out.println(t.minDistance(word1,word2));
}
public int minDistance(String word1, String word2) {
char[] ch1=word1.toCharArray();
char[] ch2=word2.toCharArray();
int m=word1.length();
int n=word2.length();
//如果有一个空串 直接返回非空串的长度
if (m*n==0){
return m+n;
}
//dp[m][n]是最小距离
int[][] dp=new int[m+1][n+1];
//边界条件
for (int i=0;i<m;i++){
dp[i][0]=i;
}
for (int j=0;j<n;j++){
dp[0][j]=j;
}
for (int i=1;i<=m;i++){
for (int j=1;j<=n;j++){
if (ch1[i-1]==ch2[j-1]){
dp[i][j]=dp[i-1][j-1];
}else {
dp[i][j]=Math.min(Math.min(dp[i-1][j],dp[i][j-1]),dp[i-1][j-1])+1;
}
}
}
return dp[m][n];
}
}
(2)只有两个键的键盘(650)
最初记事本上只有一个字符 ‘A’ 。你每次可以对这个记事本进行两种操作:
Copy All(复制全部):复制这个记事本中的所有字符(不允许仅复制部分字符)。
Paste(粘贴):粘贴 上一次 复制的字符。
给你一个数字 n ,你需要使用最少的操作次数,在记事本上输出 恰好 n 个 ‘A’ 。返回能够打印出 n 个 ‘A’ 的最少操作次数。
输入:3
输出:3
解释:
最初, 只有一个字符 'A'。
第 1 步, 使用 Copy All 操作。
第 2 步, 使用 Paste 操作来获得 'AA'。
第 3 步, 使用 Paste 操作来获得 'AAA'
输入:n = 1
输出:0
通过打表找规律:如果n为质数,则步骤为n;如果n为合数,则步骤为到达最大公约数的步骤+n/最大公约数,最大公约数又可以作为
n再次计算。
//数学规律
public class test {
public static void main(String[] args) {
int n=12;
test t=new test();
System.out.println(t.minSteps(n));
}
public int minSteps(int n) {
if (n==1){
return 0;
}
//如果是素数 直接返回数值
if (isPrimeNumber(n)){
return n;
}
int res=0;//最小操作数
for (int i=2;i*i<=n;i++){
//如果n可以整除i 操作数+i n=n/i继续计算
while (n%i==0){
n/=i;
res+=i;
}
}
//如果最后n为素数 操作数+n
if (n>1){
res+=n;
}
return res;
}
private boolean isPrimeNumber(int n){
if (n==2||n==3){
return true;
}else if(n%2==0){
return false;
}else {
for (int i=3;i<=Math.sqrt(n);i+=2){
if (n%i==0){
return false;
}
}
}
return true;
}
}
//动态规划
public class test {
public static void main(String[] args) {
int n=12;
test t=new test();
System.out.println(t.minSteps(n));
}
public int minSteps(int n) {
//dp[n]表示最小操作数
int[] dp=new int[n+1];
for (int i=2;i<=n;i++){
dp[i]=Integer.MAX_VALUE;
for (int j=1;j*j<=i;j++){
if (i%j==0){
dp[i]=Math.min(dp[i],dp[j]+i/j);
dp[i]=Math.min(dp[i],dp[i/j]+j);
}
}
}
return dp[n];
}
}
(3)正则表达式匹配(10)
给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 ‘.’ 和 ‘’ 的正则表达式匹配。
‘.’ 匹配任意单个字符
'’ 匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。
输入:s = "aa", p = "a"
输出:false
解释:"a" 无法匹配 "aa" 整个字符串。
输入:s = "aa", p = "a*"
输出:true
解释:因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。
输入:s = "ab", p = ".*"
输出:true
解释:".*" 表示可匹配零个或多个('*')任意字符('.')。
public class test {
public static void main(String[] args) {
String s="aa";
String p="a*";
test t=new test();
System.out.println(t.isMatch(s,p));
}
public boolean isMatch(String s, String p) {
int m=s.length();
int n=p.length();
char[] ch1=s.toCharArray();
char[] ch2=p.toCharArray();
boolean[][] dp=new boolean[m+1][n+1];
dp[0][0]=true;//两个空字符串可以匹配成功
for (int i=0;i<=m;i++){
for (int j=1;j<=n;j++){
if (ch2[j-1]=='*'){
dp[i][j]=dp[i][j-2];
if (matches(ch1,ch2,i,j-1)){
dp[i][j]=dp[i][j]||dp[i-1][j];
}
}else {
if (matches(ch1,ch2,i,j)){
dp[i][j]=dp[i-1][j-1];
}
}
}
}
return dp[m][n];
}
public boolean matches(char[] ch1,char[] ch2,int i,int j){
if (i==0){
return false;
}
if (ch2[j-1]=='.'){
return true;
}
return ch1[i-1]==ch2[j-1];
}
}
7.股票交易
(1)买卖股票的最佳时机(121)
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0。
//暴力解法会超时
public class test {
public static void main(String[] args) {
int[] arr={7,6,4,3,1};
test t=new test();
System.out.println(t.maxProfit(arr));
}
public int maxProfit(int[] prices) {
int n= prices.length;
if (n<2){
return 0;
}
int maxPri=0;
for (int i=0;i<n-1;i++){
for (int j=i+1;j<n;j++){
maxPri=Math.max(maxPri,prices[j]-prices[i]);
}
}
return maxPri;
}
}
//设置一个动态变化的最大利润 和 当前最小价格
public class test {
public static void main(String[] args) {
int[] arr={7,6,4,3,1};
test t=new test();
System.out.println(t.maxProfit(arr));
}
public int maxProfit(int[] prices) {
int n= prices.length;
if (n<2){
return 0;
}
int maxPri=0;//最大利润
int minPrice=Integer.MAX_VALUE;//当前遍历到的最小价格
for (int i=0;i<n;i++){
//如果当前遍历到的值小于之前遍历过的里边的最小值 就将当前值设置为遍历过的最小值
if (minPrice>prices[i]){
minPrice=prices[i];
} else if (prices[i]-minPrice>maxPri) {
//如果当前值大于之前遍历过的里边的最小值并且 prices[i]-minPrice>maxPri
maxPri=prices[i]-minPrice;//更新最大利润
}
}
return maxPri;
}
}
(2)买卖股票的最佳时机 II(122)
给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大 利润 。
输入:prices = [7,1,5,3,6,4]
输出:7
解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3 。
总利润为 4 + 3 = 7 。
输入:prices = [1,2,3,4,5]
输出:4
解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
总利润为 4 。
输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 交易无法获得正利润,所以不参与交易可以获得最大利润,最大利润为 0 。
(1)本题可以使用贪心的思想:因为不限制交易次数,只要今天的股价比昨天的高就可以进行交易。
(2)动态规划
//贪心
public class test {
public static void main(String[] args) {
int[] arr={7,6,4,3,1};
test t=new test();
System.out.println(t.maxProfit(arr));
}
public int maxProfit(int[] prices) {
int n=prices.length;
if(n<2) {
return 0;
}
int maxPro=0;//最大利润
//遍历每一个价格 当比前一个价格高时就进行交易
for(int i=1;i<prices.length;i++) {
maxPro+=Math.max(0, prices[i]-prices[i-1]);
}
return maxPro;
}
}
//动态规划
public class test {
public static void main(String[] args) {
int[] arr={7,1,5,3,6,4};
test t=new test();
System.out.println(t.maxProfit(arr));
}
public int maxProfit(int[] prices) {
int n=prices.length;
if(n<2) {
return 0;
}
int[][] dp=new int[n][2];
dp[0][0]=0;//第0天没有购入股票的时候的利润为0
dp[0][1]=-prices[0];//第0天如果购入股票的利润为-prices[0]
//遍历每一天求持有或不持有股票的最大利润
for (int i=1;i<n;i++){
//第i天交易完后不持有股票的最大利润
//前一天没有股票,则利润为前一天的利润;前一天有股票,则利润为前一天的利润+卖出获得的利润
//求两者的较大的那个作为当前的最大利润
dp[i][0]=Math.max(dp[i-1][0],dp[i-1][1]+prices[i]);
//第i天交易完后持有股票的最大利润
//前一天没有股票,则利润为前一天的利润-购入股票的钱;前一天有股票,则利润为前一天的利润
//求两者的较大的那个作为当前的最大利润
dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]-prices[i]);
}
return dp[n-1][0];//全部交易结束后持有股票的收益一定低于不持有股票的收益
}
}
(3)买卖股票的最佳时机 III(123)
给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
输入:prices = [3,3,5,0,0,3,1,4]
输出:6
解释:在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。
随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3 。
输入:prices = [1,2,3,4,5]
输出:4
解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
输入:prices = [7,6,4,3,1]
输出:0
解释:在这个情况下, 没有交易完成, 所以最大利润为 0。
public class test {
public static void main(String[] args) {
int[] arr={3,3,5,0,0,3,1,4};
test t=new test();
System.out.println(t.maxProfit(arr));
}
public int maxProfit(int[] prices) {
int n=prices.length;
if(n<2) {
return 0;
}
int buy1=-prices[0];//只进行过一次买操作
int sell1=0;//进行了一次买操作和一次卖操作
int buy2=-prices[0];//在完成了一次交易的前提下,进行了第二次买操作
int sell2=0;//完成了全部两笔交易
for (int i=1;i<n;i++){
buy1=Math.max(buy1,-prices[i]);
sell1=Math.max(sell1,buy1+prices[i]);
buy2=Math.max(buy2,sell1-prices[i]);
sell2=Math.max(sell2,buy2+prices[i]);
}
return sell2;
}
}
(4)买卖股票的最佳时机 IV
给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
输入:k = 2, prices = [2,4,1]
输出:2
解释:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。
输入:k = 2, prices = [3,2,6,5,0,3]
输出:7
解释:在第 2 天 (股票价格 = 2) 的时候买入,在第 3 天 (股票价格 = 6) 的时候卖出, 这笔交易所能获得利润 = 6-2 = 4 。
随后,在第 5 天 (股票价格 = 0) 的时候买入,在第 6 天 (股票价格 = 3) 的时候卖出, 这笔交易所能获得利润 = 3-0 = 3
public class test {
public static void main(String[] args) {
int[] arr={3,2,6,5,0,3};
int k=2;
test t=new test();
System.out.println(t.maxProfit(k,arr));
}
public int maxProfit(int k,int[] prices) {
int n=prices.length;
if(n<2) {
return 0;
}
int[] buy=new int[k+1];
for (int i=0;i<=k;i++){
buy[i]=-prices[0];
}
int[] sell=new int[k+1];
for (int i=1;i<n;i++){
for (int j=1;j<=k;j++){
buy[j]=Math.max(buy[j],sell[j-1]-prices[i]);
sell[j]=Math.max(sell[j],buy[j]+prices[i]);
}
}
return sell[k];
}
}
(5)最佳买卖股票时机含冷冻期(309)
给定一个整数数组prices,其中第 prices[i] 表示第 i 天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
输入: prices = [1,2,3,0,2]
输出: 3
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
输入: prices = [1]
输出: 0
public class test {
public static void main(String[] args) {
int[] arr={1,2,3,0,2};
test t=new test();
System.out.println(t.maxProfit(arr));
}
public int maxProfit(int[] prices) {
int n=prices.length;
if (n<2){
return 0;
}
//dp[i][0]:手上持有一支股票的最大收益
//dp[i][1]:手上不持有股票,并且处于冷冻期的累计最大收益
//dp[i][2]:手上不持有股票,并且不在冷冻期的累计最大收益
int[][] dp=new int[n][3];
dp[0][0]=-prices[0];//第0天持有的股票只能是第0天买入的
dp[0][1]=0;//不存在冷冻期
dp[0][2]=0;//不持有股票 最大收益为0
for (int i=1;i<n;i++){
//第i-1天已经持有 第i天买入,则i-1天不能持有股票并且不处于冷冻期 两者的最大收益
dp[i][0]=Math.max(dp[i-1][0],dp[i-1][2]-prices[i]);
//在第i天卖出股票 则i-1天必须持有股票
dp[i][1]=dp[i-1][0]+prices[i];
//当天没有任何操作 则i-1天不持有股票
dp[i][2]=Math.max(dp[i-1][1],dp[i-1][2]);
}
return Math.max(dp[n-1][1],dp[n-1][2]);
}
}
(6) 买卖股票的最佳时机含手续费(714)
给定一个整数数组 prices,其中 prices[i]表示第 i 天的股票价格 ;整数 fee 代表了交易股票的手续费用。=
你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。
返回获得利润的最大值。
注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。
输入:prices = [1, 3, 2, 8, 4, 9], fee = 2
输出:8
解释:能够达到的最大利润:
在此处买入 prices[0] = 1
在此处卖出 prices[3] = 8
在此处买入 prices[4] = 4
在此处卖出 prices[5] = 9
总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8
输入:prices = [1,3,7,5,10,3], fee = 3
输出:6
//动态规划
public class test {
public static void main(String[] args) {
int[] arr={1,3,7,5,10,3};
int n=3;
test t=new test();
System.out.println(t.maxProfit(arr,n));
}
public int maxProfit(int[] prices, int fee) {
int n=prices.length;
//dp[i][0]表示持有股票 dp[i][1]表示不持有股票
int[][] dp=new int[n][2];
//第0天持有股票只能是在第0天购入的
dp[0][0]=-prices[0];
dp[0][1]=0;
for (int i=1;i<n;i++){
dp[i][0]=Math.max(dp[i-1][0],dp[i-1][1]-prices[i]);
dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]+prices[i]-fee);
}
return Math.max(dp[n-1][0],dp[n-1][1]);
}
}
8.练习
(1)打家劫舍 II(213)
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
输入:nums = [1,2,3,1]
输出:4
解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
输入:nums = [1,2,3]
输出:3
public class test {
public static void main(String[] args) {
int[] arr={1,3,1,3,100};
test t=new test();
System.out.println(t.rob(arr));
}
public int rob(int[] nums) {
int n= nums.length;
if (n<1){
return 0;
}
if (n==1){
return nums[0];
}
if (n==2){
return Math.max(nums[0],nums[1]);
}
//因为首尾相连,只有三个房间的话也只能偷其中一个
if (n==3){
return Math.max(Math.max(nums[0],nums[1]),nums[2]);
}
//要保证第一间房和最后一间房不能同时偷窃
//可以将最后一间房和第一间房分到两个数组中,分别求两个数组的最大值,然后取两个最大值中较大的
int[] arr1=new int[n-1];
int[] arr2=new int[n-1];
for (int i=0;i<n-1;i++){
arr1[i]=nums[i];
}
for (int j=0;j<n-1;j++){
arr2[j]=nums[j+1];
}
int[] dp1=new int[n];//0~n-1
int[] dp2=new int[n];//1~n
dp1[0]=arr1[0];
dp1[1]=Math.max(arr1[0],arr1[1]);
dp2[0]=arr2[0];
dp2[1]=Math.max(arr2[0],arr2[1]);
for (int i=2;i<arr1.length;i++){
//如果选择偷第i间房,则第i-1间房不能偷;如果选择不偷,则偷盗的金额为偷前i-1间房的最大金额
dp1[i]=Math.max(dp1[i-1],dp1[i-2]+arr1[i]);
dp2[i]=Math.max(dp2[i-1],dp2[i-2]+arr2[i]);
}
return Math.max(dp1[arr1.length-1],dp2[arr2.length-1]);
}
}
//降低空间复杂度
public class test {
public static void main(String[] args) {
int[] arr={1,3,1,3,100};
test t=new test();
System.out.println(t.rob(arr));
}
public int rob(int[] nums) {
int n= nums.length;
if (n==1){
return nums[0];
}
if (n==2){
return Math.max(nums[0],nums[1]);
}
//因为首尾相连,只有三个房间的话也只能偷其中一个
if (n==3){
return Math.max(Math.max(nums[0],nums[1]),nums[2]);
}
return Math.max(maxMoney(nums,0,n-2),maxMoney(nums,1,n-1));
}
//因为每间房屋的最高金额只和前两间的最高金额相关,所以可以考虑使用两个变量代替动态规划数组
private int maxMoney(int[] nums, int i, int n) {
int first=nums[i];
int second=Math.max(nums[i],nums[i+1]);
for (int j=i+2;j<=n;j++){
int temp=second;
second=Math.max(first+nums[j],second);
first=temp;
}
return second;
}
}
(2)最大子数组和(53)
给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组 是数组中的一个连续部分。
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
输入:nums = [1]
输出:1
输入:nums = [5,4,-1,7,8]
输出:23
(1)贪心:若指针所指元素之前的和小于0,则丢弃当前元素之前的数列
(2)动态规划:若前一个元素大于0,则将其加到当前元素上
//贪心
public class test {
public static void main(String[] args) {
int[] arr={-2,1,-3,4,-1,2,1,-5,4};
test t=new test();
System.out.println(t.maxSubArray(arr));
}
public int maxSubArray(int[] nums) {
int n= nums.length;
if (n==1){
return nums[0];
}
//单独数组元素全部小于0的情况
int lessMax=Integer.MIN_VALUE;
for (int i=0;i<n;i++){
lessMax=Math.max(lessMax,nums[i]);
}
//如果数组中最大元素都小于0,就返回这个最大元素
if (lessMax<0){
return lessMax;
}
int sum=nums[0];//当前元素之前数列的和
int max=nums[0];//连续子数组最大和
for (int i=1;i<n;i++){
if (sum<0){//如果当前元素之前的数列和小于0,就丢弃
sum=0;
}
sum=sum+nums[i];
max=Math.max(max,sum);
}
return max;
}
}
//动态规划
public class test {
public static void main(String[] args) {
int[] arr={-2,1,-3,4,-1,2,1,-5,4};
test t=new test();
System.out.println(t.maxSubArray(arr));
}
public int maxSubArray(int[] nums) {
int n= nums.length;
if (n==1){
return nums[0];
}
//dp[i]表示:以nums[i]结尾的连续子数组的最大和
int[] dp=new int[n];
dp[0]=nums[0];//以0结尾的的最大和就是数组的第一个元素的值
for (int i=1;i<n;i++){
if (dp[i-1]<=0){
dp[i]=nums[i];
}else {
dp[i]=nums[i]+dp[i-1];
}
}
int max=dp[0];//连续数组的最大和
for (int i=1;i<n;i++){
max=Math.max(max,dp[i]);
}
return max;
}
}
(3)整数拆分(343)
给定一个正整数 n ,将其拆分为 k 个 正整数 的和( k >= 2 ),并使这些整数的乘积最大化。
返回 你可以获得的最大乘积 。
输入: n = 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。
输入: n = 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。
public class test {
public static void main(String[] args) {
int n=10;
test t=new test();
System.out.println(t.integerBreak(n));
}
public int integerBreak(int n) {
//dp[n]表示最大乘积
int[] dp=new int[n+1];
//遍历从2到n的所有的数 求出每一个的最大乘积
for (int i=2;i<=n;i++){
int curMax=0;//当前i的最大乘积
for (int j=1;j<i;j++){
curMax=Math.max(curMax,Math.max(j*(i-j),j*dp[i-j]));
}
dp[i]=curMax;
}
return dp[n];
}
}
(4)两个字符串的删除操作(583)
给定两个单词 word1 和 word2 ,返回使得 word1 和 word2 相同所需的最小步数。
每步 可以删除任意一个字符串中的一个字符。
输入: word1 = "sea", word2 = "eat"
输出: 2
解释: 第一步将 "sea" 变为 "ea" ,第二步将 "eat "变为 "ea"
输入:word1 = "leetcode", word2 = "etco"
输出:4
//动态规划
public class test {
public static void main(String[] args) {
String s1="leetcode";
String s2="etco";
test t=new test();
System.out.println(t.minDistance(s1,s2));
}
public int minDistance(String word1, String word2) {
char[] ch1=word1.toCharArray();
char[] ch2=word2.toCharArray();
int m= ch1.length;
int n= ch2.length;
//dp[m][n]表示最小步数
int[][] dp=new int[m+1][n+1];
//当某个字符串为空串时 最小步数就是令一字符串的长度
for (int i=0;i<= m;i++){
dp[i][0]=i;
}
for (int j=0;j<= n;j++){
dp[0][j]=j;
}
//遍历两个字符串的每个字符 求最小步数
for (int i=1;i<= ch1.length;i++){
for (int j=1;j<= ch2.length;j++){
//当增加一个公共字符时 最少删除操作次数不变
if (ch1[i-1]==ch2[j-1]){
dp[i][j]=dp[i-1][j-1];
}else {
//如果当前字符不是公共字符 就取将word1的当前字符去掉 或 word2的当前字符去掉时
//使word1和word2相同所需要的最小步数+1
dp[i][j]=Math.min(dp[i-1][j],dp[i][j-1])+1;
}
}
}
return dp[m][n];
}
}
//最长公共子序列
public class test {
public static void main(String[] args) {
String s1="leetcode";
String s2="etco";
test t=new test();
System.out.println(t.minDistance(s1,s2));
}
public int minDistance(String word1, String word2) {
char[] ch1=word1.toCharArray();
char[] ch2=word2.toCharArray();
int m= ch1.length;
int n= ch2.length;
//dp[m][n]表示最长公共子序列
int[][] dp=new int[m+1][n+1];
//遍历两个字符串的每个字符 求以每个字符结尾的最长公共子序列
for (int i=1;i<= ch1.length;i++){
for (int j=1;j<= ch2.length;j++){
//当增加一个公共字符时 最长公共子序列在之前的基础上+1
if (ch1[i-1]==ch2[j-1]){
dp[i][j]=dp[i-1][j-1]+1;
}else {
//如果不相同 最长公共子序列就是word1前一个字符时的最长公共子序列
//和word2前一个字符时的最长公共子序列 两者中最大的那个
dp[i][j]=Math.max(dp[i-1][j],dp[i][j-1]);
}
}
}
//最小删除步数就是 两个字符串分别减去最长公共子序列 然后相加
return m+n-2*dp[m][n];
}
}
(5)最长数对链(646)
给出 n 个数对。 在每一个数对中,第一个数字总是比第二个数字小。
现在,我们定义一种跟随关系,当且仅当 b < c 时,数对(c, d) 才可以跟在 (a, b) 后面。我们用这种形式来构造一个数对链。
给定一个数对集合,找出能够形成的最长数对链的长度。你不需要用到所有的数对,你可以以任何顺序选择其中的一些数对来构造。
输入:[[1,2], [2,3], [3,4]]
输出:2
解释:最长的数对链是 [1,2] -> [3,4]
(1)贪心:将将数组根据元素的第二个数字升序排序,然后遍历数组每找到某个元素的第二个数字小于下一个元素的第一个
数字,就将数对链的长度+1
(2)动态规划
//贪心
public class test {
public static void main(String[] args) {
int[][] arr={{1,2},{2,3},{3,4}};
test t=new test();
System.out.println(t.findLongestChain(arr));
}
public int findLongestChain(int[][] pairs) {
int maxLength=1;//最长数对链的长度
//将数组pairs根据元素数组的第二个元素进行升序 如果第二个元素相等则按照第一个元素升序
Arrays.sort(pairs, (o1, o2) -> {
if (o1[1]==o2[1]){
return o1[0]-o2[0];
}
return o1[1]-o2[1];
});
int res=pairs[0][1];//当前所在小数组的第二个值的大小
for (int i=0;i< pairs.length-1;i++){
//如果当前所在小数组的第二个值小于下一个小数组的第一个值,证明它们可以组成数对链
if (res<pairs[i+1][0]){
//res值变为下一个小数组的第二个值
res=pairs[i+1][1];
//数对链长度+1
maxLength++;
}
}
return maxLength;
}
}
//动态规划
public class test {
public static void main(String[] args) {
int[][] arr={{1,2},{2,3},{3,4}};
test t=new test();
System.out.println(t.findLongestChain(arr));
}
public int findLongestChain(int[][] pairs) {
int n= pairs.length;
Arrays.sort(pairs,(a,b)->a[0]-b[0]);
int[] dp=new int[n];
Arrays.fill(dp,1);//数组元素初始化均为1 因为每一个元素都是一个长度为1的数对链
for (int i=0;i<n;i++){
for(int j=0;j<i;j++){
if (pairs[i][0]> pairs[j][1]){
dp[i]=Math.max(dp[i],dp[j]+1);
}
}
}
return dp[n-1];
}
}
(6)摆动序列(376)
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。
例如, [1, 7, 4, 9, 2, 5] 是一个 摆动序列 ,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。
相反,[1, 4, 7, 2, 5] 和 [1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。
给你一个整数数组 nums ,返回 nums 中作为 摆动序列 的 最长子序列的长度 。
输入:nums = [1,7,4,9,2,5]
输出:6
解释:整个序列均为摆动序列,各元素之间的差值为 (6, -3, 5, -7, 3) 。
输入:nums = [1,17,5,10,13,15,10,5,16,8]
输出:7
解释:这个序列包含几个长度为 7 摆动序列。
其中一个是 [1, 17, 10, 13, 10, 16, 8] ,各元素之间的差值为 (16, -7, 3, -3, 6, -8) 。
输入:nums = [1,2,3,4,5,6,7,8,9]
输出:2
(1)贪心:因为可以删除某些元素从而得到最大摆动序列,所以可以采用贪心的策略。从左到右遍历数组,得到摆动序列,
不符合摆动序列的元素直接忽略。
(2)动态规划
//贪心
public class test {
public static void main(String[] args) {
int[] arr={1,7,4,9,2,5};
test t=new test();
System.out.println(t.wiggleMaxLength(arr));
}
public int wiggleMaxLength(int[] nums) {
int n= nums.length;
if (n<2){
return n;
}
if (n==2&&nums[0]!=nums[1]){
return 2;
}
int preDiff=nums[1]-nums[0];//两数的差值
int maxLength=preDiff==0?1:2;//摆动序列的最大长度
for (int i=1;i<n-1;i++){
int diff=nums[i+1]-nums[i];//当前两数的差值
//如果满足摆动序列的条件
if ((preDiff<=0&&diff>0)||(preDiff>=0&&diff<0)){
//改变两数的差值为当前差值
preDiff=diff;
//摆动序列的最大长度+1
maxLength++;
}
}
return maxLength;
}
}
//动态规划
public class test {
public static void main(String[] args) {
int[] arr={1,7,4,9,2,5};
test t=new test();
System.out.println(t.wiggleMaxLength(arr));
}
public int wiggleMaxLength(int[] nums) {
int n= nums.length;
if (n<2){
return n;
}
if (n==2&&nums[0]!=nums[1]){
return 2;
}
int[] up=new int[n];//上升摆动序列
int[] down=new int[n];//下降摆动序列
up[0]=down[0]=1;//一个元素也可以是一个摆动序列
for (int i=1;i<n;i++){
if (nums[i]>nums[i-1]){
up[i]=Math.max(up[i-1],down[i-1]+1);
down[i]=down[i-1];
} else if (nums[i]<nums[i-1]) {
up[i]=up[i-1];
down[i]=Math.max(up[i-1]+1,down[i-1]);
}else {
up[i]=up[i-1];
down[i]=down[i-1];
}
}
return Math.max(up[n-1],down[n-1]);
}
}
(7)目标和(494)
给你一个整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 ‘+’ 或 ‘-’ ,然后串联起所有整数,可以构造一个 表达式 :
例如,nums = [2, 1] ,可以在 2 之前添加 ‘+’ ,在 1 之前添加 ‘-’ ,然后串联起来得到表达式 “+2-1” 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 3 。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3
输入:nums = [1], target = 1
输出:1
(1)递归:因为每个元素都可以添加 "+" "-" 两个符号,题目可以等价于将数组中的元素任意相加或相减,最后得出目标
target的所有方法数
(2)动态规划
//递归
public class test {
int count=0;
public static void main(String[] args) {
int[] arr={1,1,1,1,1};
int n=3;
test t=new test();
System.out.println(t.findTargetSumWays(arr,n));
}
public int findTargetSumWays(int[] nums, int target) {
recursion(nums,target,0,0);
return count;
}
private void recursion(int[] nums, int target, int index, int sum) {
if (index== nums.length){
if (sum==target){
count++;
}
}else {
recursion(nums,target,index+1,sum+nums[index]);
recursion(nums,target,index+1,sum-nums[index]);
}
}
}
//动态规划
public class test {
int count=0;
public static void main(String[] args) {
int[] arr={1,1,1,1,1};
int n=3;
test t=new test();
System.out.println(t.findTargetSumWays(arr,n));
}
public int findTargetSumWays(int[] nums, int target) {
int sum=0;//数组元素的和
int n=nums.length;
for (int i=0;i<n;i++){
sum+=nums[i];
}
int diff=sum-target;
if (diff<0||diff%2!=0){
return 0;
}
int neg=diff/2;
int[][] dp=new int[n+1][neg+1];
dp[0][0]=1;
for (int i=1;i<=n;i++){
int num=nums[i-1];
for (int j=0;j<=neg;j++){
dp[i][j]=dp[i-1][j];
if (j>=num){
dp[i][j]+=dp[i-1][j-num];
}
}
}
return dp[n][neg];
}
}