leetcode(动态规划专题)

动态规划

  • 1.导论
  • 2.基础题目
    • 2.1 509. 斐波那契数
    • 2.2 70. 爬楼梯
    • 2.3 746. 使用最小花费爬楼梯
    • 2.4 62. 不同路径
    • 2.5 63. 不同路径 II
    • 2.6 343. 整数拆分
    • 2.7 96. 不同的二叉搜索树
    • 2.8 2110. 股票平滑下跌阶段的数目
    • 2.9 2100. 适合打劫银行的日子
  • 3.背包问题
    • 3.1 416. 分割等和子集
    • 3.2 1049. 最后一块石头的重量 II
    • 3.3 494. 目标和
    • 3.4 474. 一和零
    • 3.5 518. 零钱兑换 II
    • 3.6 377. 组合总和 Ⅳ
    • 3.7 322. 零钱兑换
    • 3.8 279. 完全平方数
    • 3.9 139. 单词拆分
  • 4.打家劫舍
    • 4.1 198.打家劫舍
    • 4.2 213.打家劫舍II
    • 4.3 337. 打家劫舍 III
  • 5.买卖股票
    • 5.1 121. 买卖股票的最佳时机
    • 5.2 122.买卖股票的最佳时机II
    • 5.3 123.买卖股票的最佳时机III
    • 5.4 188.买卖股票的最佳时机IV
    • 5.5 309. 最佳买卖股票时机含冷冻期
    • 5.6 714. 买卖股票的最佳时机含手续费
  • 6.子序列
    • 6.1 300. 最长递增子序列
    • 6.2 674. 最长连续递增序列
    • 6.3 718. 最长重复子数组
    • 6.4 1143. 最长公共子序列
    • 6.5 1035. 不相交的线
    • 6.6 53. 最大子数组和
    • 6.7 392. 判断子序列
    • 6.8 115. 不同的子序列
    • 6.9 583. 两个字符串的删除操作
    • 6.10 72. 编辑距离
    • 6.11 647. 回文子串
    • 6.12 516. 最长回文子序列

1.导论

  • 动态规划解题步骤:

1.确定dp数组以及下标的含义
2.确定递推公式
3.dp数组如何初始化
4.确定遍历顺序

  • 优化:滚动数组

  • 背包问题

01背包:每个物品的数量只有一个,不选,选。
先遍历背包或物品都可以

/*
	二维数组
	核心代码
	先遍历物品后遍历背包
	dp[i][j]表示下标为【0-i】的物品里任意取,放进容量为j的背包,价值总和最大是多少
*/
for(int i=1;i<weight.length;++i){
	for(int j=0;j<=bagWeight;++j){
		if(j<weight[i]){
			dp[i][j] = dp[i-1][j]
		}else {
			dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]);
		}
	}
}

/*
	先遍历背包后遍历物品
*/
for(int j=0;j<=bagWeight;++j){
	for(int i=1;i<weight.length;++i){
		if(j<weight[i]){
			dp[i][j] = dp[i-1][j]
		}else {
			dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]);
		}
	}
}

/*
	滚动数组优化
	dp[j]表示容量为j的背包所能背的最大价值总和为多少
	先遍历物品后遍历背包
	遍历背包时,逆序遍历防止覆盖
*/
for(int i=1;i<weight.length;++i){
	for(int j=bagWeight;j>=weight[i];--j){
		dp[j] = Math.max(dp[[j],dp[j-weight[i]]+value[i]);
	}
}

完全背包:每个物品的数量有无数个,不选,选几个
求组合数:先遍历物品后遍历背包
求排列数:先遍历背包后遍历物品

/*
	完全背包就是物品可以重复利用
	相对于01背包,完全背包都是顺序遍历
	对于纯完全背包,先遍历物品后遍历背包,或则先遍历背包后遍历物品都可以	
*/
for(int i=1;i<weight.length;++i){//遍历物品
	for(int j=weight[i];j<=bagweight;++j){//遍历背包
		dp[j] = Math.max(dp[[j],dp[j-weight[i]]+value[i]);
	}
}


for(int j=0;j<=bagweight;++j){//遍历背包
	for(int i=0;i<weight.length;++i){//遍历物品
		if(j>=weigth[i]){
			dp[j] = Math.max(dp[[j],dp[j-weight[i]]+value[i]);
		}
	}
}

多重背包:不同的物品,数量不同
01背包的进化版本

//核心代码
for(int i=1;i<weight.length;++i){//遍历物品
	for(int j=bagWeight;j>=weight[i];--j){//遍历背包
		//以上是01背包
		for(int k=1;k<=nums[i] && (j-k*weight[i)>=0;++k){
			dp[j] = Math.max(dp[[j],dp[j-k*weight[i]]+value[i]);
		}
	}
}

2.基础题目

2.1 509. 斐波那契数

斐波那契数,通常用 F(n) 表示,形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。
0 <= n <= 30

class Solution {
    public int fib(int n) {
        /*
            1.dp[i]表示第i个数的斐波那契数
                int[] dp = new int[n+1];
            2.dp[i] = dp[i-1]+dp[i-2];
            3.dp[0] = 0,dp[1] = 1;
            4.从递推关系式可以看出,dp[i]是由dp[i-1]和dp[i-2]推导出来的,所以是顺序遍历
         */
         if(n<2){
             return n;
         }
         int[] dp = new int[n+1];
         dp[0] = 0;
         dp[1] = 1;
         for(int i=2;i<=n;++i){
            dp[i] = dp[i-1]+dp[i-2];
         }
         return dp[n];
    }
}

//优化
class Solution {
    public int fib(int n) {
         if(n<2){
             return n;
         }
         int one = 0;
         int two = 1;
         for(int i=2;i<=n;++i){
            int sum = one + two;
            one = two;
            two = sum;
         }
         return two;
    }
}

2.2 70. 爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

注意:给定 n 是一个正整数。

class Solution {
    public int climbStairs(int n) {
        if(n==1 || n==2) return n;
        int one = 1;
        int two = 1;
        for(int i=2;i<=n;++i){
            int sum = one + two;
            two = one;
            one = sum;
        }
        return one;
    }
}

2.3 746. 使用最小花费爬楼梯

数组的每个下标作为一个阶梯,第 i 个阶梯对应着一个非负数的体力花费值 cost[i](下标从 0 开始)。

每当你爬上一个阶梯你都要花费对应的体力值,一旦支付了相应的体力值,你就可以选择向上爬一个阶梯或者爬两个阶梯。

请你找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为 0 或 1 的元素作为初始阶梯。

cost 的长度范围是 [2, 1000]。
cost[i] 将会是一个整型数据,范围为 [0, 999] 。

class Solution {
    public int minCostClimbingStairs(int[] cost) {
        /*
            1.dp[i]表示第i个台阶所花费的最少体力
                int[] dp = new int[cost.length];
            2.dp[i] = Math.min(dp[i-1],dp[i-2])+cost[i];
            3.dp[0] = cost[0];dp[1] = cost[1];
            4.顺序遍历
         */
         int[] dp = new int[cost.length];
         dp[0] = cost[0];
         dp[1] = cost[1];
         for(int i=2;i<cost.length;++i){
             dp[i] = Math.min(dp[i-1],dp[i-2])+cost[i];
         }
         return Math.min(dp[cost.length-1],dp[cost.length-2]);

    }
}

//优化
class Solution {
    public int minCostClimbingStairs(int[] cost) {
         int one = cost[0];
         int two = cost[1];
         for(int i=2;i<cost.length;++i){
             int sum = Math.min(one,two)+cost[i];
             one = two;
             two = sum;
         }
         return Math.min(one,two);

    }
}


class Solution {
    public int minCostClimbingStairs(int[] cost) {
        int n = cost.length;
        int[] dp = new int[cost.length + 1];                                  
        for (int i = 2; i <= n; i++) {
            dp[i] = Math.min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);
        }
        return dp[n];
    }
}

class Solution {
    public int minCostClimbingStairs(int[] cost) {
        int n = cost.length;
        int pre1 = 0;
        int pre2 = 0;                                  
        for (int i = 2; i <= n; i++) {
            int cur = Math.min(pre2+cost[i-1],pre1+cost[i-2]);
            pre1 = pre2;
            pre2 = cur;
        }
        return pre2;
    }
}

2.4 62. 不同路径

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

问总共有多少条不同的路径?
1 <= m, n <= 100
题目数据保证答案小于等于 2 * 109

class Solution {
    public int uniquePaths(int m, int n) {
        /*
            1.dp[i][j]表示到达(i,j)位置共有多少条不同路径
                int[][] dp = new int[m][n];
            2.只能从上方或则左方走来:dp[i][j] = dp[i-1][j] + dp[i][j-1]
            3.上边界和左边界初始化为1;
            4.顺序遍历
        */
        int[][] dp = new int[m][n];
        dp[0][0] = 1;
        for(int i=1;i<n;++i){
            dp[0][i] = 1;
        }
        for(int i=1;i<m;++i){
            dp[i][0] = 1;
        }
        for(int i=1;i<m;++i){
            for(int j=1;j<n;++j){
                dp[i][j] = dp[i-1][j] + dp[i][j-1];
            }
        }
        return dp[m-1][n-1];
    }
}
//优化:滚动数组思想
class Solution {
    public int uniquePaths(int m, int n) {
        int[] dp = new int[m];//缓存当前行
        for(int i=0;i<m;++i){
            dp[i] = 1;
        }
        for(int i=1;i<n;++i){
            for(int j=1;j<m;++j){
                dp[j] = dp[j] + dp[j-1];
            }
        }
        return dp[m-1];
    }
}

class Solution {
    public int uniquePaths(int m, int n) {
        int[] dp = new int[n];//缓存当前列
        for(int i=0;i<n;++i){
            dp[i] = 1;
        }
        for(int i=1;i<m;++i){
            for(int j=1;j<n;++j){
                dp[j] = dp[j] + dp[j-1];
            }
        }
        return dp[n-1];
    }
}

2.5 63. 不同路径 II

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。

现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
m == obstacleGrid.length
n == obstacleGrid[i].length
1 <= m, n <= 100
obstacleGrid[i][j] 为 0 或 1

/*
	1.确定dp数组的下标及其含义
		dp[i][j]表示从左上角到达(i,j)位置的路径数
		int[][] dp = new int[m][n];
	2.确定递推公式
		由于机器人只能向下或则向右移动,因此要想到达(i,j)的位置,则一定是从(i-1,j)或则(i,j-1)走过来的
		如果obstacleGrid[i][j] = 1时,即本身有障碍,则dp[i][j] = 0;
		如果obstacleGrid[i][j] = 0时,即本身没有障碍,则 dp[i][j] = dp[i-1][j] + dp[i][j-1];
	3.初始化
		当i=0是,如果这一行有障碍,则障碍之前的路径数都为1,从障碍开始之后的路径数都为0
		同理当j=0时,如果这一列有障碍,则障碍之前的路径数都为1,从障碍开始之后的路径数都为0
	4.确定遍历顺序
		由递推公式可以看到dp[i][j]是由dp[i-1][j]和dp[i][j-1]推导出来的,因此是顺序遍历
	5.最终结果
		可知到达最后位置就是数组的最后一个值即dp[m-1][n-1];
		
*/
class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int m = obstacleGrid.length;
        int n = obstacleGrid[0].length;
        int[][] dp = new int[m][n];
        for(int i=0;i<m;++i){
            if(obstacleGrid[i][0] == 0){
                dp[i][0] = 1;
            }else{
                break;
            }
        }
        for(int i=0;i<n;++i){
            if(obstacleGrid[0][i] == 0){
                dp[0][i] = 1;
            }else{
                break;
            }
        }
        for(int i=1;i<m;++i){
            for(int j=1;j<n;++j){
                if(obstacleGrid[i][j] == 1){
                    continue;
                }
                dp[i][j] = dp[i-1][j] + dp[i][j-1];
            }
        }
        return dp[m-1][n-1];
    }
}

/*
	优化:
		由于这里dp[i][j]只和dp[i-1][j]与dp[i][j-1]有关,我们可以运用「滚动数组思想」把空间复杂度优化
	1.确定dp数组的下标及其含义
		dp[j]用来缓存当前行
		int[] dp = new int[n];
	2.确定递推公式
		由于机器人只能向下或则向右移动,因此要想到达第i行,则一定是从上一行(j-1)或则当前行(j)走过来的
		如果obstacleGrid[i][j] = 1时,即本身有障碍,则dp[j] = 0;
		如果obstacleGrid[i][j] = 0时,即本身没有障碍,
			此时还要考虑j-1>=0,并且obstacleGrid[i][j-1] = 0
			则 dp[j] = dp[j-1]+dp[j]
	3.初始化
		由递推公式可知,其基础都是由dp[0]推导来的
		因此dp[0] = obstacleGrid[0][0] == 0 ? 1 : 0;
	4.确定遍历顺序
		由递推公式可以看到dp[j]是由dp[j-1]和dp[j]推导出来的,因此是顺序遍历
	5.最终结果
		可知到达最后位置就是数组的最后一个值即dp[n-1];
*/
class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
    	int m = obstacleGrid.length;
        int n = obstacleGrid[0].length;
        int[] dp = new int[n];
        dp[0] = obstacleGrid[0][0] == 0 ? 1 : 0;
        for(int i=0;i<m;++i){
			for(int j=0;j<n;++j){
				if(obstacleGrid[i][j] == 1){
					dp[j] = 0;
					continue;
				}
				if(j-1>=0 && obstacleGrid[i][j-1] == 0){
					dp[j] = dp[j-1]+dp[j];
				}
			}
		}
		return dp[n-1];
    }
}

/*
	同理
		dp[j]用来缓存当前列
		int[] dp = new int[m];	
*/
class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
    	int m = obstacleGrid.length;
        int n = obstacleGrid[0].length;
        int[] dp = new int[m];
        dp[0] = obstacleGrid[0][0] == 0 ? 1 : 0;
        for(int i=0;i<n;++i){
			for(int j=0;j<m;++j){
				if(obstacleGrid[j][i] == 1){
					dp[j] = 0;
					continue;
				}
				if(j-1>=0 && obstacleGrid[j-1][i] == 0){
					dp[j] = dp[j-1]+dp[j];
				}
			}
		}
		return dp[m-1];
    }
}

2.6 343. 整数拆分

给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。
你可以假设 n 不小于 2 且不大于 58。

class Solution {
    public int integerBreak(int n) {
        /*
            1.dp[i]表示分拆数字i可以得到的最大乘积
                int[] dp = new int[n+1];
            2.dp[i] = Math.max(dp[i],Math.max((i-j)*j,dp[i-j]*j));
            3.dp[0] = 0;dp[1] =0;
            4.顺序遍历
         */
         int[] dp = new int[n+1];
         for(int i=2;i<=n;++i){
             for(int j=1;j<i;++j){
                 dp[i] = Math.max(dp[i],Math.max((i-j)*j,dp[i-j]*j));
             }
         }
         return dp[n];
    }
}

2.7 96. 不同的二叉搜索树

给你一个整数 n ,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。
1 <= n <= 19

class Solution {
    public int numTrees(int n) {
        /*
            假设n个节点存在二叉排序树的个数是G(n),令f(i)为以i为根的二叉搜索树的个数
            即有:G(n) = f(1) + f(2) + f(3) + f(4) + ... + f(n)
            n为根节点,当i为根节点时,其左子树节点个数为[1,2,3,...,i-1],右子树节点个 数 为[i+1,i+2,...n],所以当i为根节点时,其左子树节点个数为i-1个,右子树节点为n-i,即f(i) = G(i-1)*G(n-i),

			上面两式可得:G(n) = G(0)*G(n-1)+G(1)*(n-2)+...+G(n-1)*G(0)

            1.dp[i]表示i个结点组成的二叉搜索树的个数为dp[i];
                int[] dp = new int[n+1];
            2.dp[i] = dp[i] + d[j-1]*dp[i-j];
                j-1为j为头结点左子树结点数量,i-j为j为头结点右子树结点数量
            3.dp[0] = 1;dp[1] = 1;
            4.顺序遍历
         */
         int[] dp = new int[n+1];
         dp[0] = 1;
         dp[1] = 1;
         for(int i=2;i<=n;++i){
             for(int j=1;j<=i;++j){
                 dp[i] = dp[i] + dp[j-1]*dp[i-j];
             }
         }
         return dp[n];
    }
}

2.8 2110. 股票平滑下跌阶段的数目

给你一个整数数组 prices ,表示一支股票的历史每日股价,其中 prices[i] 是这支股票第 i 天的价格。

一个 平滑下降的阶段 定义为:对于 连续一天或者多天 ,每日股价都比 前一日股价恰好少 1 ,这个阶段第一天的股价没有限制。

请你返回 平滑下降阶段 的数目。
1 <= prices.length <= 105
1 <= prices[i] <= 105

class Solution {
    public long getDescentPeriods(int[] prices) {
        /**
            1.dp[i]表示以第i天结尾的平滑下降阶段的数目
            2.如果 prices[i] + 1 == prices[i-1],则dp[i] = dp[i-1] +1;
                否者dp[i] = 1;
            3.dp[0] = 1;
            4.顺序遍历
         */
        int n = prices.length;
        int[] dp = new int[n];
        dp[0] = 1;
        long ans = 1;
        for (int i = 1; i < n; ++i) {
            if (prices[i] + 1 == prices[i-1]) {
                dp[i] = dp[ i-1] +1;
            } else {
                dp[i] = 1;
            }
            ans += dp[i];
        }
        return ans;
    }
}


//优化
class Solution {
    public long getDescentPeriods(int[] prices) {
        int n = prices.length;
        int pre = 1;
        long ans = 1;
        for (int i = 1; i < n; ++i) {
            if (prices[i] + 1 == prices[i-1]) {
                ++pre;
            } else {
                pre = 1;
            }
            ans += pre;
        }
        return ans;
    }
}

2.9 2100. 适合打劫银行的日子

你和一群强盗准备打劫银行。给你一个下标从 0 开始的整数数组 security ,其中 security[i] 是第 i 天执勤警卫的数量。日子从 0 开始编号。同时给你一个整数 time 。

如果第 i 天满足以下所有条件,我们称它为一个适合打劫银行的日子:

第 i 天前和后都分别至少有 time 天。
第 i 天前连续 time 天警卫数目都是非递增的。
第 i 天后连续 time 天警卫数目都是非递减的。
更正式的,第 i 天是一个合适打劫银行的日子当且仅当:security[i - time] >= security[i - time + 1] >= … >= security[i] <= … <= security[i + time - 1] <= security[i + time].

请你返回一个数组,包含 所有 适合打劫银行的日子(下标从 0 开始)。返回的日子可以 任意 顺序排列。
1 <= security.length <= 105
0 <= security[i], time <= 105

class Solution {
    public List<Integer> goodDaysToRobBank(int[] security, int time) {
        int n = security.length;
        List<Integer> list = new ArrayList<>();
        if (2 * time + 1 > n) return list;
        if (time == 0) {
            for (int i = 0; i < n; ++i) list.add(i);
            return list;
        }
        
        int[] left = new int[n];// 记录i点左边非递增的元素数目
        int[] right = new int[n];// 记录i点右边非递减的元素数目
        for (int i = 1; i < n; ++i) {
            if (security[i-1] >= security[i]) {
                left[i] = left[i-1] +1;
            }
        }

        for (int i = n - 2; i >=0; --i) {
            if (security[i] <= security[i+1]) {
                right[i] = right[i+1] +1;
            }
        }

        
        for (int i = 0; i < n; ++i) {
            if (left[i] >= time && right[i] >= time) {
                list.add(i);
            }
        }
        return list; 

    }
}

3.背包问题

3.1 416. 分割等和子集

给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
1 <= nums.length <= 200
1 <= nums[i] <= 100

class Solution {
    public boolean canPartition(int[] nums) {
        /*
            类似01背包
            1.dp[i][j]表示从数组的[0,i]下标范围内选取若干个正整数(可以是 0 个),是否存在一种选取方案使得被选取的正整数的和等于j
            2.如果j>=nums[i],则对于当前的数字nums[i],可以选也可以不选,两种情况只要有一个true,机油dp[i][j]= true;
                如果不选取,则dp[i][j] = dp[i-1][j];
                如果选取,则dp[i][j] = dp[i-1][j-nums[i]];
              如果j
        int n=nums.length;
        if(n<2) return false;
        int sum=0;//记录数组总和
        int maxNum = 0;//记录数组中最大值
        for(int num : nums){
            sum += num;
            maxNum = Math.max(maxNum,num);
        }
        if(sum%2 != 0) return false;//如果sum不能被2整除,说明不能分割
        int target = sum/2;
        if(maxNum > target) return false;//如果数组中存在大于数组总和一半的值,不能分割
        boolean[][] dp = new boolean[n][target+1];
        dp[0][nums[0]] = true;
        for(int i=1;i<n;++i){
            int num = nums[i];
            for(int j=1;j<=target;++j){
                if(j>=num){
                    dp[i][j] = dp[i-1][j] || dp[i-1][j-num];
                }else{
                    dp[i][j] = dp[i-1][j];
                }
            }
        }
        return dp[n-1][target];

    }
}
//优化:滚动数组
class Solution {
    public boolean canPartition(int[] nums) {
        /*
            类似01背包
            1.dp[i][j]表示从数组的[0,i]下标范围内选取若干个正整数(可以是 0 个),是否存在一种选取方案使得被选取的正整数的和等于j
            2.如果j>=nums[i],则对于当前的数字nums[i],可以选也可以不选,两种情况只要有一个true,机油dp[i][j]= true;
                如果不选取,则dp[i][j] = dp[i-1][j];
                如果选取,则dp[i][j] = dp[i-1][j-nums[i]];
              如果j
        int n=nums.length;
        if(n<2) return false;
        int sum=0;//记录数组总和
        int maxNum = 0;//记录数组中最大值
        for(int num : nums){
            sum += num;
            maxNum = Math.max(maxNum,num);
        }
        if(sum%2 != 0) return false;//如果sum不能被2整除,说明不能分割
        int target = sum/2;
        if(maxNum > target) return false;//如果数组中存在大于数组总和一半的值,不能分割
        boolean[] dp = new boolean[target+1];
        dp[0] = true;
        for(int i=1;i<n;++i){
            int num = nums[i];
            for(int j=target;j>=num;--j){//倒叙遍历保证元素只使用一次
                dp[j] = dp[j] || dp[j-num];
            }
        }
        return dp[target];

    }
}

//01背包
//dp[i]表示容量为i的背包,所背的物品的最大价值
//物品是nums[i],重量是nums[i],价值是nums[i],背包容量是sum/2
class Solution {
    public boolean canPartition(int[] nums) {
        if(nums.length < 2) return false;
        int sum = 0;
        int maxNum = 0;
        for(int num : nums){
            sum += num;
            maxNum = Math.max(maxNum,num);
        }
        if(sum % 2 != 0) return false;
        int target = sum/2;
        if(maxNum > target) return false;
        int[] dp = new int[target+1];
        for(int i=0;i<nums.length;++i){
            for(int j=target;j>=nums[i];--j){
                dp[j] =Math.max(dp[j],dp[j-nums[i]]+nums[i]);
            }
        }
        return dp[target] == target;
    }
}

3.2 1049. 最后一块石头的重量 II

有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。

每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:

如果 x == y,那么两块石头都会被完全粉碎;
如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0。

1 <= stones.length <= 30
1 <= stones[i] <= 100

class Solution {
    public int lastStoneWeightII(int[] stones) {
        /*
        	本题其实就是尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小
            1.dp[i]表示容量为i的背包所能背的石头的最大重量
            2.dp[i] = Math.max(dp[i],dp[i-stones[j]]+stones[j])
            3.背包如果为0,取哪些物品价值总和一定为0
            4.先遍历物品,后遍历背包(更好理解)
              也可以先遍历背包,再遍历物品
         */
        if(stones.length == 1) return stones[0];
        int sum = 0;
        for(int stone : stones){
            sum += stone;
        }
        int target = sum/2;
        int[] dp = new int[target+1];
        for(int i=0;i<stones.length;++i){//遍历物品
            for(int j=target;j>=stones[i];--j){//逆序遍历防止重复使用
                dp[j] = Math.max(dp[j],dp[j-stones[i]]+stones[i]);
            }
        }
        return sum - 2*dp[target];
    }   
}

3.3 494. 目标和

给你一个整数数组 nums 和一个整数 target 。

向数组中的每个整数前添加 ‘+’ 或 ‘-’ ,然后串联起所有整数,可以构造一个 表达式 :

例如,nums = [2, 1] ,可以在 2 之前添加 ‘+’ ,在 1 之前添加 ‘-’ ,然后串联起来得到表达式 “+2-1” 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目
1 <= nums.length <= 20
0 <= nums[i] <= 1000
0 <= sum(nums[i]) <= 1000
-1000 <= target <= 1000

class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        /*
            转换为01背包问题:
                假设加法的总和为x,那么减法的总和为sum-x
                则可知x-(sum-x) = target;
                x = (target+sum)/2(注意此时若target+sum为奇数则无解)

            1.dp[j]表示填满容量为j的背包的方法数
            2.d[j] = dp[j] + dp[j-nums[i]];
            3.dp[0] = 1;
            4.先遍历物品,后遍历背包,外顺内逆
         */
         int sum = 0;
         for(int num : nums){
             sum += num;
         }
         if(Math.abs(target) > sum) return 0;
         if((target + sum) % 2 != 0) return 0;
         int bagSize = (target + sum) / 2;
         int[] dp = new int[bagSize+1];
         dp[0] = 1;
         for(int i=0;i<nums.length;++i){
             for(int j=bagSize;j>=nums[i];--j){
                 dp[j] = dp[j] + dp[j-nums[i]];
             }
         }
         return dp[bagSize];
    }
}

3.4 474. 一和零

给你一个二进制字符串数组 strs 和两个整数 m 和 n 。

请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。

如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。
1 <= strs.length <= 600
1 <= strs[i].length <= 100
strs[i] 仅由 ‘0’ 和 ‘1’ 组成
1 <= m, n <= 100

class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        /*
            数组中的元素就是物品
            m和n相当于一个背包,两个维度的背包
            字符串中0和1的个数相当于重量,字符串本身的个数相当于价值

            1.dp[i][j]表示最多有i个0和j个1的最大子集大小
            2.dp[i][j]可以由上一个strs里的字符串推导出来,strs里的字符串0和1的数量分别为zeroNum,oneNum
                dp[i][j] = dp[i-zeroNum][j-oneNum] +1;
                因为要取最大值,因此
                dp[i][j] = Math.max(dp[i][j],dp[i-zeroNum][j-oneNum] +1);
            3.因为物品价值不会为负数,初始为0,保证递推的时候dp[i][j]不会被初始值覆盖
            4.先遍历物品后遍历背包,遍历物品顺序,遍历背包(二维)逆序
         */
        int[][] dp = new int[m+1][n+1];
        for(String str : strs){//遍历物品
            int zeroNum =0;
            int oneNum = 0;

            //统计当前字符串中0和1的数量
            for(int k=0;k<str.length();++k){
                char c = str.charAt(k);
                if(c == '0') {
                    ++zeroNum;
                }else {
                    ++oneNum;
                }                
            }
             //遍历背包
            for(int i=m;i>=zeroNum;--i){
                for(int j=n;j>=oneNum;--j){
                    dp[i][j] = Math.max(dp[i][j],dp[i-zeroNum][j-oneNum] +1);
                }
            }
        }
        return dp[m][n];
    }
}

3.5 518. 零钱兑换 II

给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。

请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。

假设每一种面额的硬币有无限个。

题目数据保证结果符合 32 位带符号整数。

1 <= coins.length <= 300
1 <= coins[i] <= 5000
coins 中的所有值 互不相同
0 <= amount <= 5000

class Solution {
    public int change(int amount, int[] coins) {
        /*
            物品:coins[i] 
                每个物品数量无限个,完全背包
            背包:amount

            1.dp[j]表示凑成金额为j的方法数
            2.dp[j] = dp[j]+dp[j-coins[i]];
            3.dp[0] = 1;
            4.先遍历物品后遍历背包,顺序遍历
         */
         int[] dp = new int[amount+1];
         dp[0] = 1;
         for(int i=0;i<coins.length;++i){
             for(int j=coins[i];j<=amount;++j){
                 dp[j] = dp[j]+dp[j-coins[i]];
             }
         }
         return dp[amount];
    }
}

3.6 377. 组合总和 Ⅳ

给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。

题目数据保证答案符合 32 位整数范围。
1 <= nums.length <= 200
1 <= nums[i] <= 1000
nums 中的所有元素 互不相同
1 <= target <= 1000

class Solution {
    public int combinationSum4(int[] nums, int target) {
        /**
            物品: nums[i]; 
                每个物品可以重复使用,完全背包
            背包:target

            1.dp[j]表示目标整数为j时元素组合的个数
            2.dp[j] =  dp[j] + dp[j-nums[i]];
            3.dp[0] = 1;
            4.求得是排列数,即(1,3)和(3,1)是两种情况,因此外层遍历背包,内层遍历物品,顺序遍历
         */
         int[] dp = new int[target+1];
         dp[0] = 1;
         for(int i=1;i<=target;++i){
             for(int j=0;j<nums.length;++j){
                 if(i>=nums[j]){
                     dp[i] = dp[i] + dp[i-nums[j]];
                 }
             }
         }
         return dp[target];
    }
}

3.7 322. 零钱兑换

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。

你可以认为每种硬币的数量是无限的
1 <= coins.length <= 12
1 <= coins[i] <= 231 - 1
0 <= amount <= 104

class Solution {
    public int coinChange(int[] coins, int amount) {
        /**
            物品:coins[i];
                每个物品可以重复使用,完全背包
            背包:amount

            1.dp[j]表示凑足金额j所需最少硬币数
            2.dp[j] = Math.min(dp[j],dp[j-coins[i]]+1);
            3.dp[0] = 0;考虑到递推公式的特性,dp[j]必须初始为一个最大的数,否则会被覆盖
            4.外循环物品,内循环背包,顺序遍历
         */
         int[] dp = new int[amount+1];
         dp[0] = 0;
         for(int i=1;i<dp.length;++i){
             dp[i] = Integer.MAX_VALUE;
         }
         for(int i=0;i<coins.length;++i){
             for(int j=coins[i];j<=amount;++j){
                 if(dp[j-coins[i]] != Integer.MAX_VALUE){
                    dp[j] = Math.min(dp[j],dp[j-coins[i]]+1);
                 }
             }
         }
         return dp[amount] == Integer.MAX_VALUE ? -1 : dp[amount];
    }
}

3.8 279. 完全平方数

给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, …)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。

给你一个整数 n ,返回和为 n 的完全平方数的 最少数量 。

完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
1 <= n <= 104

class Solution {
    public int numSquares(int n) {
        /*
            物品:完全平方数;
                每个物品可以重复使用,完全背包
            背包:n

            1.dp[i]表示和为i需要的完全平方数的最少个数
            2.dp[i] = Math.min(dp[i],dp[i-j*j]+1);
            3.dp[0] = 0;其他值初始为最大值,防止覆盖
            4.外层遍历物品,内层遍历背包
         */
         int[] dp = new int[n+1];
         dp[0] = 0;
         for(int i=1;i<dp.length;++i){
             dp[i] = Integer.MAX_VALUE;
         }
         for(int i=1;i*i<=n;++i){
             for(int j=i*i;j<=n;++j){
                 if(dp[j-i*i] != Integer.MAX_VALUE){
                     dp[j] = Math.min(dp[j],dp[j-i*i]+1);
                 }
             }
         }
         return dp[n];
    }
}

3.9 139. 单词拆分

给你一个字符串 s 和一个字符串列表 wordDict 作为字典,判定 s 是否可以由空格拆分为一个或多个在字典中出现的单词。

说明:拆分时可以重复使用字典中的单词。
1 <= s.length <= 300
1 <= wordDict.length <= 1000
1 <= wordDict[i].length <= 20
s 和 wordDict[i] 仅有小写英文字母组成
wordDict 中的所有字符串 互不相同

class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        /**
            物品:单词
                可以重复使用,完全背包
            背包:字符串s

            1.dp[i]:字符串长度为i时,是否可以由空格拆分为一个或多个在字典中出现的单词
            2.如果确定dp[j]是true,则[j,i]这个区间的子串出现在字典里,那么dp[i]一定是true(j
         Set<String> wordSet = new HashSet<>(wordDict);
         boolean[] dp = new boolean[s.length()+1];
         dp[0] = true;
         for(int i=1;i<=s.length();++i){
             for(int j=0;j<i;++j){//保证j
                 String word = s.substring(j,i);
                 if(wordSet.contains(word) && dp[j]){
                     dp[i] = true;
                     break;
                 }
             }
         }
         return dp[s.length()];
    }
}

4.打家劫舍

4.1 198.打家劫舍

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
1 <= nums.length <= 100
0 <= nums[i] <= 400

/*
	1.确定dp数组的下标及其含义
		dp[i] 表示前i间房屋能偷窃到的最高总金额
	2.递推公式
		可知每个房间都有偷或则不偷两种状态
			当不偷第i间房时,则偷窃总金额是前i-1间房的总金额
			当偷第i间房时,则第i-1间房不能偷,因此偷窃总金额为前i-2间房的总金额加上第i间房的金额
		则可知能偷到的最高金额 dp[i] = max(dp[i-1],d[i-2]+nums[i]])
	3.初始化
		考虑边界条件
			只有一件房时
			dp[0] = nums[0];
			有两件房时
			dp[1] = max(nums[0],nums[1]);
	4.确定顺序
		由递推公式可知是顺序遍历
	5.最终结果
		dp[nums.length-1];
*/
class Solution {
    public int rob(int[] nums) {
		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-1],dp[i-2]+nums[i]);
		}
		return dp[nums.length-1];
	}
}

/*
	优化
	由递推公式dp[i] = max(dp[i-1],d[i-2]+nums[i]])可知
	dp[i]只与dp[i-1]和dp[i-2]有关,所以可以了用滚动数组的思想对空间进行优化
*/
class Solution {
    public int rob(int[] nums) {
		if(nums.length == 1){
			return nums[0];
		}
		int one = nums[0];
		int two = Math.max(nums[0],nums[1]);
		for(int i=2;i<nums.length;++i){
			int temp = two;
			two = Math.max(two,one+nums[i]);
			one = temp;
		}
		return two;
	}
}

4.2 213.打家劫舍II

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。

给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
1 <= nums.length <= 100
0 <= nums[i] <= 1000

/*
	此题房子首尾相连,我们要考虑首尾的情况
	则此时我们可偷的范围变成了(0,nums.length-2)或(1,nums.length-1)
	其中每种情况又变成了打家劫舍1中的情况了
*/
class Solution {
    public int rob(int[] nums) {
        int length = nums.length;
        if(length == 1){
            return nums[0];
        }else if(length == 2){
            return Math.max(nums[0],nums[1]);
        }
        return Math.max(robRange(nums,0,length-2),robRange(nums,1,length-1));
        
    }
    public int robRange(int[] nums,int start,int end){
        int[] dp = new int[nums.length];
		dp[start] = nums[start];
		dp[start+1] = Math.max(nums[start],nums[start+1]);
		for(int i=start+2;i<=end;++i){
			dp[i] = Math.max(dp[i-1],dp[i-2]+nums[i]);
		}
		return dp[end];
	}
}


/*
	对方法一进行优化
*/
class Solution {
    public int rob(int[] nums) {
        int length = nums.length;
        if(length == 1){
            return nums[0];
        }else if(length == 2){
            return Math.max(nums[0],nums[1]);
        }
        return Math.max(robRange(nums,0,length-2),robRange(nums,1,length-1));
        
    }
    public int robRange(int[] nums,int start,int end){
        int one = nums[start];
        int two = Math.max(nums[start],nums[start+1]);
        for(int i = start+2;i<=end;++i){
            int temp = two;
            second = Math.max(one+nums[i],two);
            one = temp;
        }
        return two;
    }
}

4.3 337. 打家劫舍 III

在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。

计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    Map<TreeNode,Integer> robMap = new HashMap<>();//保存抢劫该结点的结果
    Map<TreeNode,Integer> unrobMap = new HashMap<>();//保存不抢劫该结点的结果
    public int rob(TreeNode root) {
        dfs(root);
        return Math.max(robMap.get(root),unrobMap.get(root));
    }
    private void dfs(TreeNode root){
        if(root == null){
            return;
        }
        dfs(root.left);
        dfs(root.right);
        robMap.put(root,root.val+unrobMap.getOrDefault(root.left,0)+unrobMap.getOrDefault(root.right,0));
        unrobMap.put(root,Math.max(robMap.getOrDefault(root.left,0),unrobMap.getOrDefault(root.left,0))+Math.max(robMap.getOrDefault(root.right,0),unrobMap.getOrDefault(root.right,0)));
    }
}
//优化
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public int rob(TreeNode root) {
        int[] money = dfs(root);
        return Math.max(money[0],money[1]);
    }
    private int[] dfs(TreeNode root){
        if(root == null){
            return new int[]{0,0};
        }
        int[] left = dfs(root.left);
        int[] right = dfs(root.right);
        int steal = root.val + left[1] + right[1];
        int unsteal = Math.max(left[0],left[1]) + Math.max(right[0],right[1]);
        return new int[]{steal,unsteal};
    }
}

5.买卖股票

5.1 121. 买卖股票的最佳时机

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
1 <= prices.length <= 105
0 <= prices[i] <= 104

```java
class Solution {
    public int maxProfit(int[] prices) {
        /*
            思路:
                1.确定dp数组及其下标含义
                    dp[i][0]表示第i天不持有股票,所获得最大利润
                    dp[i][1]表示第i天持有股票,所获得最大利润
                    int[][] dp = new int[prices.length][2];
                2.确定递推数组
                    (1)如果第i天不持有股票即dp[i][0],那么可以由两个状态推出来:
                        ①第i-1天就不持有股票,就保持现状,则利润为dp[i-1][0];
                        ②第i-1天持有股票,所得利润就是今天卖出后所得的利润即:
                        dp[i-1][1]+prices[i];
                        则最大利润dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]+prices[i]);
                    (3)如果第i天持有股票即dp[i][1],那么可以由两个状态推出来:
                        ①第i-1天就持有股票,则保持现状,则利润为dp[i-1][1];
                        ②第i-1天不持有股票,则今天买入,所获得利润为-prices[i];
                        则最大利润dp[i][1] = Math.max(dp[i-1][1],-prices[i]);
                3.dp数组初始化
                    由递推公式 dp[i][0] =Math.max(dp[i-1][0], prices[i] + dp[i-1][1]);
                    和 dp[i][1] = Math.max(dp[i-1][1], -prices[i]);可以看出
                    其基础都是要从dp[0][0]和dp[0][1]推导出来。
                        dp[0][0]表示第0天不持有股票,不持有股票那么现金就是0,所以dp[0][0] = 0;
                        dp[0][1]表示第0天持有股票,此时的持有股票就一定是买入股票了,因为不可能由前
                        一天推出来,所以dp[0][1] = -prices[0];
                4.确定遍历顺序
                    从递推公式可以看出dp[i]都是有dp[i-1]推导出来的,那么一定是从前向后遍历。
                5.最终结果
                    最后要获得最大收益,最后的状态一定是不持有股票,所以最终的结果为
                    dp[prices.length-1][0]
                
         */
         int[][] dp = new int[prices.length][2];
         dp[0][0] = 0;
         dp[0][1] = -prices[0];
         for(int i=1;i<prices.length;++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],-prices[i]);
         }
         return dp[prices.length-1][0];
    }
}

/*
	对方法一进行优化
	从递推公式可以看出,dp[i]只是依赖于dp[i - 1]的状态。
		dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]+prices[i]);
		dp[i][1] = Math.max(dp[i-1][1],-prices[i]);
	那么我们只需要记录 当前天的dp状态和前一天的dp状态就可以了,可以使用滚动数组来节省空间
*/
class Solution {
    public int maxProfit(int[] prices) {
		 int no = 0;
         int yes = -prices[0];
         for(int i=1;i<prices.length;++i){
             no = Math.max(no,yes+prices[i]);
             yes = Math.max(yes,-prices[i]); 
         }
         return no;
    } 
}

//贪心算法
class Solution {
    public int maxProfit(int[] prices) {
        int min = Integer.MAX_VALUE;
        int result = 0;
        for(int i=0;i<prices.length;++i){
            min = Math.min(min,prices[i]);
            result = Math.max(result,prices[i]-min);
        }
        return result;
    }
}

5.2 122.买卖股票的最佳时机II

给定一个数组 prices ,其中 prices[i] 是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
1 <= prices.length <= 3 * 104
0 <= prices[i] <= 104

/*
	1.确定dp数组以及下标的含义
		dp[i][0] 表示第i天不持有股票所得最多现金
		dp[i][1] 表示第i天持有股票所得最多现金
		int[][] dp = new int[price.length][2]; 
	2.确定递推公式
		如果第i天不持有股票即dp[i][0], 也可以由两个状态推出来
			第i-1天就不持有股票,那么就保持现状,所得现金就是昨天不持有股票的所得现金 即:dp[i - 1][0]
			第i天卖出股票,所得现金就是按照今天股票佳价格卖出后所得现金即:prices[i] + dp[i - 1][1]
			dp[i][0]取最大的,dp[i][0] = max(dp[i - 1][0], prices[i] + dp[i - 1][1]);
		如果第i天持有股票即dp[i][1], 那么可以由两个状态推出来
			第i-1天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金 即:dp[i - 1][1]
			第i天买入股票(说明第i-1天是不持有股票的),所得现金就是买入今天的股票后所得现金与i-1天不持有股票所得现金之和即:dp[i-1][0]-prices[i]
			那么dp[i][0]应该选所得现金最大的,所以dp[i][1] = max(dp[i - 1][0], dp[i-1][0]-prices[i]);
	3.dp数组如何初始化
		由递推公式 dp[i][0] = max(dp[i - 1][0], prices[i] + dp[i - 1][1]);和 dp[i][1] = max(dp[i - 1][0], dp[i-1][0]-prices[i]);可以看出
		其基础都是要从dp[0][0]和dp[0][1]推导出来。
		dp[0][0]表示第0天不持有股票,不持有股票那么现金就是0,所以dp[0][0] = 0;
		dp[0][1]表示第0天持有股票,此时的持有股票就一定是买入股票了,因为不可能有前一天推出来,所以dp[0][1] = -prices[0];
	4.确定遍历顺序
		从递推公式可以看出dp[i]都是有dp[i - 1]推导出来的,那么一定是从前向后遍历。
	5.最终结果
		最后要获得最大收益,最后的状态一定是不持有股票,所以最终的结果为dp[prices.length-1][0]
*/
class Solution {
    public int maxProfit(int[] prices) {
   		int[][] dp = new int[prices.length][2];
   		dp[0][0] = 0;
   		dp[0][1] = -prices[0];
   		for(int i=1;i<prices.length;++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]);
		}
		return dp[prices.length-1][0];
    } 
}

/*
	对方法一进行优化
	从递推公式可以看出,dp[i]只是依赖于dp[i - 1]的状态。
		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]);
	那么我们只需要记录 当前天的dp状态和前一天的dp状态就可以了,可以使用滚动数组来节省空间
*/
class Solution {
    public int maxProfit(int[] prices) {
		int notHold = 0;
		int hold = -prices[0];
		for(int i=1;i<prices.length;++i){
			notHold = Math.max(notHold,hold+prices[i]);
			hold = Math.max(hold,notHold-prices[i]);
		} 
		return notHold;
    } 
}
//贪心算法
class Solution {
    public int maxProfit(int[] prices) {
        int ans = 0;
        for(int i=1;i<prices.length;++i){
            ans += Math.max(prices[i]-prices[i-1],0);
        }
        return ans;
    }
}

5.3 123.买卖股票的最佳时机III

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
1 <= prices.length <= 105
0 <= prices[i] <= 105

/*
	1.确定dp数组以及下标的含义
		一天一共就有五个状态, 
		没有操作
		第一次买入
		第一次卖出
		第二次买入
		第二次卖出
		dp[i][j]中 i表示第i天,j为 [0 - 4] 五个状态,dp[i][j]表示第i天状态j所剩最大现金。
		int[][] dp = new int[m][5];

	2.确定递推公式
		第i天没有操作,则和前一天没有操作状态一样
			dp[i][0] = dp[i-1][0]
		需要注意:dp[i][1],**表示的是第i天,买入股票的状态,并不是说一定要第i天买入股票。
		达到dp[i][1]状态,有两个具体操作:
			操作一:第i天买入股票了,那么dp[i][1] = dp[i-1][0] - prices[i]
			操作二:第i天没有操作,而是沿用前一天买入的状态,即:dp[i][1] = dp[i - 1][1]
			要获取最大利润,所以 dp[i][1] = max(dp[i-1][0] - prices[i], dp[i - 1][1]);
			
		同理dp[i][2]也有两个操作:
			操作一:第i天卖出股票了,那么dp[i][2] = dp[i - 1][1] + prices[i]
			操作二:第i天没有操作,沿用前一天卖出股票的状态,即:dp[i][2] = dp[i - 1][2]
			所以dp[i][2] = max(dp[i - 1][1] + prices[i], dp[i - 1][2])
			
		同理可推出剩下状态部分:
			dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i]);
		 	dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]);

	3.dp数组如何初始化
		第0天没有操作,这个最容易想到,就是0,即:dp[0][0] = 0;
		第0天做第一次买入的操作,dp[0][1] = -prices[0];
		第0天做第一次卖出的操作,这个初始值应该是多少呢?
			首先卖出的操作一定是收获利润,整个股票买卖最差情况也就是没有盈利即全程无操作现金为0,
		从递推公式中可以看出每次是取最大值,那么既然是收获利润如果比0还小了就没有必要收获这个利润了。
			所以dp[0][2] = 0;
		第0天第二次买入操作,初始值应该是多少呢?
			不用管第几次,现在手头上没有现金,只要买入,现金就做相应的减少
			所以第二次买入操作,初始化为:dp[0][3] = -prices[0];
		同理第二次卖出初始化dp[0][4] = 0;
	
	4.确定遍历顺序
		从递归公式其实已经可以看出,一定是从前向后遍历,因为dp[i],依靠dp[i - 1]的数值。
	5.最终结果
		dp[m-1][4]
*/
class Solution {
    public int maxProfit(int[] prices) {
        int m = prices.length;
        int[][] dp = new int[m][5];
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        dp[0][2] = 0;
        dp[0][3] = -prices[0];
        dp[0][4] = 0;
        for(int i=1;i<m;++i){
            dp[i][0] = dp[i-1][0];
            dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] - prices[i]);
            dp[i][2] = Math.max(dp[i-1][2], dp[i-1][1] + prices[i]);
            dp[i][3] = Math.max(dp[i-1][3], dp[i-1][2] - prices[i]);
            dp[i][4] = Math.max(dp[i-1][4], dp[i-1][3] + prices[i]);
        }
        return dp[m-1][4];
    }
}
/*
	对方法一进行优化
		由递推数组可以看出,dp[i][..]只与dp[i-1][..]有关,利用滚动数组进行优化
		int dp[] = new int[5];
*/
class Solution {
    public int maxProfit(int[] prices) {
        int m = prices.length;
        int[] dp = new int[5];
        dp[1] = -prices[0];
        dp[3] = -prices[0];
        for(int i=1;i<m;++i){
            dp[1] = Math.max(dp[1], dp[0] - prices[i]);
            dp[2] = Math.max(dp[2], dp[1] + prices[i]);
            dp[3] = Math.max(dp[3], dp[2] - prices[i]);
            dp[4] = Math.max(dp[4], dp[3] + prices[i]);
        }
        return dp[4];
    }
}
/*
	还可以再进行优化到常量空间
*/
class Solution {
    public int maxProfit(int[] prices) {
        int m = prices.length;
 		int buyFirst = -prices[0];
 		int sellFirst = 0;
        int buySecond = -prices[0];
        int sellSecond = 0;
        for(int i=1;i<prices.length;++i){
            buyFirst = Math.max(buyFirst,-prices[i]);
            sellFirst = Math.max(sellFirst, buyFirst + prices[i]);
            buySecond  = Math.max(buySecond , sellFirst - prices[i]);
            sellSecond = Math.max(sellSecond, buySecond + prices[i]);
        }
        return sellSecond;
    }
}

5.4 188.买卖股票的最佳时机IV

给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
0 <= k <= 100
0 <= prices.length <= 1000
0 <= prices[i] <= 1000

  • 方法一
/*
	对III的推广
	1.确定dp数组及其下标的含义
		dp[i][j] :表示第i天的状态为j,所剩下的最大现金是dp[i][j]
		j的状态表示为:
			0 表示不操作
			1 第一次买入
			2 第一次卖出
			3 第二次买入
			4 第二次卖出
			.....
		可以看出,除了0以外,偶数就是卖出,奇数就是买入。
		题目要求是至多有K笔交易,那么j的范围就定义为 2 * k + 1 就可以了。
		int[][] dp = new int[m][2 * k + 1];
	2.确定递推公式
		达到dp[i][1]状态,有两个具体操作:
			操作一:第i天买入股票了,那么dp[i][1] = dp[i - 1][0] - prices[i]
			操作二:第i天没有操作,而是沿用前一天买入的状态,即:dp[i][1] = dp[i - 1][1]
			选最大的,所以 dp[i][1] = max(dp[i - 1][0] - prices[i], dp[i - 1][0]);
		同理dp[i][2]也有两个操作:
			操作一:第i天卖出股票了,那么dp[i][2] = dp[i - 1][1] + prices[i]
			操作二:第i天没有操作,沿用前一天卖出股票的状态,即:dp[i][2] = dp[i - 1][2]
			所以dp[i][2] = max(dp[i - 1][i] + prices[i], dp[i][2])
		同理......
			dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-1] - prices[i]); //奇数情况
    		dp[i][j+1] = Math.max(dp[i-1][j+1], dp[i-1][j] + prices[i]);//偶数情况
    3.初始化
    	第0天没有操作,这个最容易想到,就是0,即:dp[0][0] = 0;
		第0天做第一次买入的操作,dp[0][1] = -prices[0];
		第0天做第一次卖出的操作,这个初始值应该是多少呢?
			首先卖出的操作一定是收获利润,整个股票买卖最差情况也就是没有盈利即全程无操作现金为0,
		从递推公式中可以看出每次是取最大值,那么既然是收获利润如果比0还小了就没有必要收获这个利润了。
			所以dp[0][2] = 0;
		第0天第二次买入操作,初始值应该是多少呢?
			不用管第几次,现在手头上没有现金,只要买入,现金就做相应的减少
			所以第二次买入操作,初始化为:dp[0][3] = -prices[0];
		同理第二次卖出初始化dp[0][4] = 0;
		....
		可以推出当j为奇数时初始为-prices[0];偶数时为0
	4.确定遍历顺序
		从递归公式其实已经可以看出,一定是从前向后遍历,因为dp[i],依靠dp[i - 1]的数值。
	5.最终结果
		最后一次卖出,一定是利润最大的,dp[m-1][2 * k]就是最后求解。
	*/
class Solution {
    public int maxProfit(int k, int[] prices) {
    	//本题的中数组长度可以为0,所以要考虑为0的情况
        if(prices.length == 0){
            return 0;
        }
        int m = prices.length;
        int[][] dp = new int[m][2*k+1];
        for(int j=1;j<2*k;j+=2){
			dp[0][j] = -prices[0];
		}
        for(int i=1;i<m;++i){
            for(int j=1;j<2*k;j+=2){
				dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-1] - prices[i]);
    			dp[i][j+1] = Math.max(dp[i-1][j+1], dp[i-1][j] + prices[i]);
			}
        }
        return dp[m-1][2*k];
    }
}
  • 方法二
/*
	对方法一优化
*/
class Solution {
    public int maxProfit(int k, int[] prices) {
    	//本题的中数组长度可以为0,所以要考虑为0的情况
        if(prices.length == 0){
            return 0;
        }
        int m = prices.length;
        int[] dp = new int[2*k+1];
        for(int j=1;j<2*k;j+=2){
			dp[j] = -prices[0];
		}
        for(int i=1;i<m;++i){
            for(int j=1;j<2*k;j+=2){
				dp[j] = Math.max(dp[j], dp[j-1] - prices[i]);
    			dp[j+1] = Math.max(dp[j+1], dp[j] + prices[i]);
			}
        }
        return dp[2*k];
    }
}

5.5 309. 最佳买卖股票时机含冷冻期

给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。​

设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):

你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。

class Solution {
    public int maxProfit(int[] prices) {
        if(prices.length == 0){
            return 0;
        }
        int[][] dp = new int[prices.length][3];
        dp[0][0] = -prices[0];
        /*
		 dp[i][0]: 手上持有股票的最大收益
         dp[i][1]: 手上不持有股票,并且处于冷冻期中的累计最大收益
         dp[i][2]: 手上不持有股票,并且不在冷冻期中的累计最大收益
		*/
        
        for(int i=1;i<prices.length;++i){
            dp[i][0] = Math.max(dp[i-1][0],dp[i-1][2]-prices[i]);
            dp[i][1] = dp[i-1][0] + prices[i];
            dp[i][2] = Math.max(dp[i-1][1],dp[i-1][2]);
        }
        return Math.max(dp[prices.length-1][1],dp[prices.length-1][2]);
    }
}

//优化
class Solution {
    public int maxProfit(int[] prices) {
        if(prices.length == 0){
            return 0;
        }
        int dp0 = -prices[0];
        int dp1 = 0;
        int dp2 = 0;
        for(int i=1;i<prices.length;++i){
            int dp00 = Math.max(dp0,dp2-prices[i]);
            int dp11 = dp0 + prices[i];
            int dp22 = Math.max(dp1,dp2);
            dp0 = dp00;
            dp1 = dp11;
            dp2 = dp22;
        }
        return Math.max(dp1,dp2);
    }
}



5.6 714. 买卖股票的最佳时机含手续费

给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;整数 fee 代表了交易股票的手续费用。

你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。

返回获得利润的最大值。

注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。
1 <= prices.length <= 5 * 104
1 <= prices[i] < 5 * 104
0 <= fee < 5 * 104

//相比于买卖股票II多减去一个手续费
class Solution {
    public int maxProfit(int[] prices, int fee) {
        int[][] dp = new int[prices.length][2];
   		dp[0][0] = 0;
   		dp[0][1] = -prices[0];
   		for(int i=1;i<prices.length;++i){
			dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]+prices[i]-fee);
			dp[i][1] = Math.max(dp[i-1][1],dp[i-1][0]-prices[i]);
		}
		return dp[prices.length-1][0];
    }
}
//优化
class Solution {
    public int maxProfit(int[] prices,int fee) {
		int notHold = 0;
		int hold = -prices[0];
		for(int i=1;i<prices.length;++i){
			notHold = Math.max(notHold,hold+prices[i]-fee);
			hold = Math.max(hold,notHold-prices[i]);
		} 
		return notHold;
    } 
}

6.子序列

6.1 300. 最长递增子序列

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
1 <= nums.length <= 2500
-104 <= nums[i] <= 104

class Solution {
    public int lengthOfLIS(int[] nums) {
        /*
            1.dp[i]表示i之前包括i的最长严格递增子序列的长度
            2.位置i的最长递增子序列等于j从0到i-1各个位置的最长上升子序列+1的最大值
                if(num[i]>nums[j]) dp[i] = Math.max(dp[i],dp[j]+1)
            3.每一个i对应的dp[i]起始值至少都是1
            4.dp[i]是由0到i-1各个位置推导而来,因此遍历i一定是从前往后便利
                j就是0到i-1
         */
         if(nums.length <= 1) return nums.length;
         int[] dp = new int[nums.length];
         Arrays.fill(dp,1);
         int result = 0;
         for(int i=1;i<nums.length;++i){
             for(int j=0;j<i;++j){
                 if(nums[i] > nums[j]){
                     dp[i] = Math.max(dp[i],dp[j]+1);
                 }
             }
             if(dp[i] > result) result = dp[i];//取最长子序列
         }
         return result;
    }
}

class Solution {
//贪心+二分查找(更优)
    public int lengthOfLIS(Integer[] nums) {
        int len = 1;//记录最长上升子序列的长度
        int n= nums.length;
        if (n == 0) return 0;
        int[] dp = new int[n+1];//长度为i的最长上上升子序列的末尾元素的最小值
        dp[len] = nums[0];
        for (int i = 1; i < n; ++i) {
            if (nums[i] > dp[len]) {
                dp[++len] = nums[i];
            } else {
                int l = 1, r = len, pos = 0;
                while (l <= r) {
                    int mid = (l+r) >> 1;
                    if (dp[mid] < nums[i]) {
                        pos = mid;
                        l = mid + 1;
                    } else {
                        r = mid - 1;
                    }
                }
                dp[pos + 1] = nums[i];
            }
        }
        return len;
    }
 }

6.2 674. 最长连续递增序列

给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。

连续递增的子序列 可以由两个下标 l 和 r(l < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,那么子序列 [nums[l], nums[l + 1], …, nums[r - 1], nums[r]] 就是连续递增子序列。
1 <= nums.length <= 104
-109 <= nums[i] <= 109

class Solution {
    public int findLengthOfLCIS(int[] nums) {
        /**
            1.dp[i]表示以下标i结尾的数组的连续递增子序列的长度
            2.if(nums[i] > nums[i-1]) dp[i] = dp[i-1] + 1;
            3.dp[i]都初始化为1
            4.顺序便利
         */
         int[] dp = new int[nums.length];
         Arrays.fill(dp,1);
         int result = 1;
         for(int i=1;i<nums.length;++i){
             if(nums[i] > nums[i-1]) dp[i] = dp[i-1] + 1;
             result = Math.max(result,dp[i]);
         } 
         return result;
    }
}
//贪心算法
class Solution {
    public int findLengthOfLCIS(int[] nums) {
         int count = 1;
         int result = 1;
         for(int i=1;i<nums.length;++i){
             if(nums[i] > nums[i-1]){
                 ++count;
             } else {
                 count = 1;
             }
             result = Math.max(result,count);
         } 
         return result;
    }
}

6.3 718. 最长重复子数组

给两个整数数组 A 和 B ,返回两个数组中公共的、长度最长的子数组的长度。
1 <= len(A), len(B) <= 1000
0 <= A[i], B[i] < 100

class Solution {
    public int findLength(int[] nums1, int[] nums2) {
        /**
            1.dp[i][j]表示以i-1为结尾的A和以下表j-1为结尾的B最长重复子数组的长度
            2.if(nums1[i-1] == nums2[j-1]) dp[i][j] = dp[i-1][j-1] + 1;
            3.dp[i][0]和dp[0][j]初始化为0;
            4.顺序遍历
         */
         int[][] dp = new int[nums1.length+1][nums2.length+1];
         int result = 0;
         for(int i=1;i<=nums1.length;++i){
            for(int j=1;j<=nums2.length;++j){
                if(nums1[i-1] == nums2[j-1]) {
                    dp[i][j] = dp[i-1][j-1] + 1;
                }
                result = Math.max(result,dp[i][j]);
            }
            
         }
         return result;
    }
}

//滚动数组优化
class Solution {
    public int findLength(int[] nums1, int[] nums2) {
         int[] dp = new int[nums2.length+1];
         int result = 0;
                                                                                                                                                                                       for(int i=1;i<=nums1.length;++i){
            for(int j=nums2.length;j>0;--j){//从后往前便利,避免覆盖
                if(nums1[i-1] == nums2[j-1]) {
                    dp[j] = dp[j-1] + 1;
                }else {
                    dp[j] = 0;
                }
                result = Math.max(result,dp[j]);
            }
            
         }
         return result;
    }
}

6.4 1143. 最长公共子序列

给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
1 <= text1.length, text2.length <= 1000
text1 和 text2 仅由小写英文字符组成。

class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        /**
            1.dp[i][j]表示下标[0,i-1]的字符串text和下标[0,j-1]字符串text2的最长公共子序列的长度
            2.if(text1.charAt(i-1) == text2.charAt(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]);
            3.dp[i][0] = 0;dp[0][j] = 0;
            4.顺序遍历
         */
        int[][] dp = new int[text1.length()+1][text2.length()+1];
        for(int i=1;i<=text1.length();++i){
            for(int j=1;j<=text2.length();++j){
                if(text1.charAt(i-1) == text2.charAt(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[text1.length()][text2.length()];
    }
}

6.5 1035. 不相交的线

在两条独立的水平线上按给定的顺序写下 nums1 和 nums2 中的整数。

现在,可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线,这些直线需要同时满足满足:

nums1[i] == nums2[j]
且绘制的直线不与任何其他连线(非水平线)相交。
请注意,连线即使在端点也不能相交:每个数字只能属于一条连线。

以这种方法绘制线条,并返回可以绘制的最大连线数。
1 <= nums1.length, nums2.length <= 500
1 <= nums1[i], nums2[j] <= 2000

class Solution {
    public int maxUncrossedLines(int[] nums1, int[] nums2) {
        /**
            其实就和1143题:求两个字符串的最长公共子序列一样
         */
        int[][] dp = new int[nums1.length+1][nums2.length+1];
        for(int i=1;i<=nums1.length;++i){
            for(int j=1;j<=nums2.length;++j){
                if(nums1[i-1] == nums2[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[nums1.length][nums2.length];
    }
}

6.6 53. 最大子数组和

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组 是数组中的一个连续部分。

class Solution {
    public int maxSubArray(int[] nums) {
        /*
            思路:
                1.确定dp数组下标及其含义
                    dp[i]表示第i个数结尾的连续子数组的最大和
                    int[] dp = new int[nums.length];
                2.确定递推公式
                    可知dp[i]与dp[i-1]有关,
                    如果dp[i-1]<=0,则dp[i]=nums[i]
                    如果dp[i-1]>0,则dp[i]=dp[i-1]+nums[i],所以
                    dp[i] = Math.max(dp[i-1]+nums[i],nums[i]);
                3.dp数组初始化
                    由于dp[i]表示第i个数结尾的连续子数组的最大和,则dp[0]=nums[0];
                4.确定遍历顺序
                    由于dp[i]是由dp[i-1]递推出来的,可知遍历顺序由前向后
                5.最终结果
                    可知dp数组中存储的最大值就是最终结果。
         */
         int[] dp = new int[nums.length];
         dp[0] = nums[0];
         int max = dp[0];
         for(int i=1;i<nums.length;++i){
             dp[i] = Math.max(dp[i-1]+nums[i],nums[i]);
             max = Math.max(max,dp[i]);
         }
         return max;

    }
}

//贪心算法
class Solution {
    public int maxSubArray(int[] nums) {
        int ans = Integer.MIN_VALUE;
        int count = 0;
        for(int i=0;i<nums.length;++i){
            count += nums[i];
            if(count > ans){
                ans = count;
            }

            //相当于重置最大子序列起始位置,因为遇到负数一定是拉低总和(贪心的体现)
            if(count < 0) count = 0;
        }
        return ans;
    }
}

6.7 392. 判断子序列

给定字符串 s 和 t ,判断 s 是否为 t 的子序列。

字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
0 <= s.length <= 100
0 <= t.length <= 10^4
两个字符串都只由小写字符组成

class Solution {
    public boolean isSubsequence(String s, String t) {
        /**
            1.dp[i][j]表示以下表i-1结尾的字符串s和以下标j-1结尾的字符串t,相同子序列的长度
            2.if(s.charAt(i-1) == t.charAt(j-1)) dp[i][j] = dp[i-1][j-1] + 1;
                else dp[i][j] = dp[i][j-1];
            3.dp[0][0] = 0,dp[0][j] = 0;因为都是没有意义的
            4.顺序便利
         */
         int[][] dp = new int[s.length()+1][t.length()+1];
         for(int i=1;i<=s.length();++i){
             for(int j=1;j<=t.length();++j){
                 if(s.charAt(i-1) == t.charAt(j-1)){
                    dp[i][j] = dp[i-1][j-1] + 1;
                 }else {
                    dp[i][j] = dp[i][j-1];
                 }
             }
         }
         return dp[s.length()][t.length()] == s.length();
    }
}
//双指针
class Solution {
    public boolean isSubsequence(String s, String t) {
        int n = s.length(), m = t.length();
        int i = 0, j = 0;
        while (i < n && j < m) {
            if (s.charAt(i) == t.charAt(j)) {
                i++;
            }
            j++;
        }
        return i == n;
    }
}

6.8 115. 不同的子序列

给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。

字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,“ACE” 是 “ABCDE” 的一个子序列,而 “AEC” 不是)

题目数据保证答案符合 32 位带符号整数范围。
0 <= s.length, t.length <= 1000
s 和 t 由英文字母组成

class Solution {
    public int numDistinct(String s, String t) {
        /**
            1.dp[i][j]表示以下标为i-1结尾的字符串s子序列中出现以j-1结尾的字符串t的个数
            2.if(s.charAt(i-1) == t.charAt(j-1)) dp[i][j] = dp[i-1][j-1] + dp[i-1][j]
            else dp[i][j] = dp[i-1][j]
            3.dp[i][0]都为1,因为也就是把以i-1为结尾的s,删除所有元素,出现空字符串的个数就是1
            dp[0][j]为0,s无论如何也变不成t
            4.顺序遍历
         */
        int[][] dp = new int[s.length()+1][t.length()+1];
        for(int i=0;i<=s.length();++i){
            dp[i][0] = 1;
        }
        for(int i=1;i<=s.length();++i){
            for(int j=1;j<=t.length();++j){
                if(s.charAt(i-1) == t.charAt(j-1)) {
                    dp[i][j] = dp[i-1][j-1] + dp[i-1][j];
                }else {
                    dp[i][j] = dp[i-1][j];
                }
            }
        }
        return dp[s.length()][t.length()];
    }
}

6.9 583. 两个字符串的删除操作

给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符
给定单词的长度不超过500。
给定单词中的字符只含有小写字母。

class Solution {
    public int minDistance(String word1, String word2) {
        /**
            1.dp[i][j]表示以i-1结尾的字符串word1和以j-1结尾的字符串word2想要达到相等,所需要删除元素的最少次数
            2.if(word1.charAt(i-1) == word2.charAt(j-1)) dp[i][j] = dp[i-1][j-1]
            else dp[i][j] = Math.min(dp[i-1][j]+1,dp[i][j-1]+1,dp[i-1][j-1]+2)
            3.dp[-i][0] = i;dp[0][j] = j;
            4.顺序遍历
         */
        int[][] dp = new int[word1.length()+1][word2.length()+1];
        for(int i=0;i<=word1.length();++i){
            dp[i][0] = i;
        }
        for(int j=0;j<=word2.length();++j){
            dp[0][j] = j;
        }
        for(int i=1;i<=word1.length();++i){
            for(int j=1;j<=word2.length();++j){
                if(word1.charAt(i-1) == word2.charAt(j-1)) {
                    dp[i][j] = dp[i-1][j-1];
                }else {
                    dp[i][j] = Math.min(dp[i-1][j]+1,Math.min(dp[i][j-1]+1,dp[i-1][j-1]+2));
                }
            }
        }
        return dp[word1.length()][word2.length()];
    }
}

6.10 72. 编辑距离

给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。

你可以对一个单词进行如下三种操作:

插入一个字符
删除一个字符
替换一个字符
0 <= word1.length, word2.length <= 500
word1 和 word2 由小写英文字母组成

class Solution {
    public int minDistance(String word1, String word2) {
        /**
            1.dp[i][j]dp[i][j]表示以i-1结尾的字符串word1和以j-1结尾的字符串word2的最近编辑距离
            2.if(word1.charAt(i-1) == word2.charAt(j-1)) dp[i][j] = dp[i-1][j-1];
                else dp[i][j] = Math.min(dp[i-1][j-1]+1,dp[i-1][j]+1,dp[i][j-1]+1);
            3.dp[i][0] = i;dp[0][j] = j;
            4.顺序遍历
         */
        int[][] dp = new int[word1.length()+1][word2.length()+1];
        for(int i=0;i<=word1.length();++i){
            dp[i][0] = i;
        }
        for(int j=0;j<=word2.length();++j){
            dp[0][j] = j;
        }
        for(int i=1;i<=word1.length();++i){
            for(int j=1;j<=word2.length();++j){
                if(word1.charAt(i-1) == word2.charAt(j-1)) {
                    dp[i][j] = dp[i-1][j-1];
                }else {
                    dp[i][j] = Math.min(dp[i-1][j]+1,Math.min(dp[i][j-1]+1,dp[i-1][j-1]+1));
                }
            }
        }
        return dp[word1.length()][word2.length()];
    }
}

6.11 647. 回文子串

给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。

回文字符串 是正着读和倒过来读一样的字符串。

子字符串 是字符串中的由连续字符组成的一个序列。

具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
1 <= s.length <= 1000
s 由小写英文字母组

class Solution {
    public int countSubstrings(String s) {
        /**
            1.dp[i][j]表示区间[i,j]的字符串是否为回文子串
            2.if(s[i] == s[j]) {
                if(j-i <=1 ) dp[i][j] = true;
                else if(dp[i+1][j-1]){
                    dp[i][j] = true
                }
            }
            3.初始化为false;
            4.外逆序内顺序遍历
         */
        boolean[][] dp = new boolean[s.length()][s.length()];
        int result = 0;
        for(int i=s.length()-1;i>=0;--i){
            for(int j=i;j<s.length();++j){
                if(s.charAt(i)==s.charAt(j) && (j-i<=1 || dp[i+1][j-1])){
                    ++result;
                    dp[i][j] = true;
                }
            }
        }
        return result;
    }
}

//中心扩展法
class Solution {
    public int countSubstrings(String s) {
        int n = s.length();
        int res = 0;
        for(int i=0;i<2*n-1;++i){//枚举所有的子串
            int l = i/2,r = i/2+i%2;
            while(l>=0 && r<n && s.charAt(l) == s.charAt(r)){
                --l;
                ++r;
                ++res;
            }
        }
        return res;

    }
}

6.12 516. 最长回文子序列

给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。

子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。
1 <= s.length <= 1000
s 仅由小写英文字母组成

class Solution {
    public int longestPalindromeSubseq(String s) {
        /**
            1.dp[i][j]表示字符串s在[i,j]范围内最长的回文子序列的长度;
            2.if(s[i] == s[j]) dp[i][j] = dp[i+1][j-1] +2
            else dp[i][j] = Math.max(dp[i+1][j],dp[i][j-1]);
            3.初始化i与j相等的情况:dp[i][j] = 1;
            4.外逆序内顺序遍历
         */
         int n = s.length();
        int[][] dp = new int[n][n];
        for (int i = 0; i < n; ++i) {
            dp[i][i] = 1;
        }

        for (int i = n - 2; i >=0; --i) {
            for (int j = i + 1; j < n; ++j) {
                if(s.charAt(i) == s.charAt(j)) {
                    dp[i][j] = dp[i + 1][j - 1] + 2;
                }else {
                    dp[i][j] = Math.max(dp[i][j - 1], dp[i + 1][j]);
                }
            }
        }
        return dp[0][n - 1];
    }
}

你可能感兴趣的:(笔记,动态规划,leetcode,贪心算法)