1、动态规划:每一个状态一定是由之前的状态推导出来的,通过总结归纳发现递推关系
2、解决动态规划问题的步骤:
目录
6.1 成对/分成两个子集:求和/2,使得各子集趋近目标值(背包总含量),进行动态规划
6.2 组合问题: 求组合个数:动态规划,求装满背包有几种方法,一般公式都是:dp[j] += dp[j - nums[i]]; 求所有组合列表:回溯算法
6.4 完全背包:物品可多次添加,嵌套顺序均可,小到大(遍历顺序不同,01背包:先物品,在背包大到小)
8、股票获取最大收益:写出所有状态下 的所有情况下的收益取最大值,用数组存储
10. 子序列问题:公共序列(个数),或变为相等字符串(操作数),二维
- 示例:509. 斐波那契数
- class Solution {
- public int fib(int n) {
- if(n <= 1) return n;//考虑特殊情况
- int[] F = new int[n+1];//确定dp数组
- F[0] = 0;//初始化dp
- F[1] = 1;
- for(int i = 2; i <= n; i++){//确定遍历顺序
- F[i] = F[i-1] + F[i-2];//确定递推表达式
- }
- return F[n];
- }
- }
-
- 70. 爬楼梯 到n 阶有多少种方法
- class Solution {
- public int climbStairs(int n) {
- if( n <= 2) return n;
- int[] dp = new int[n+1];//n阶楼梯,存放n+1个数据,到每层个有多少种方法
- //每次你可以爬 1 或 2 个台阶
- dp[1] = 1;//只能一步
- dp[2] = 2;//两个一步,或者,一个两步
- //当前方法数 = dp[n-1] + dp[n-2] 一步到位,两步到
- for(int i = 3; i<=n; i++){
- dp[i] = dp[i-1] +dp[i-2];
- }
- return dp[n];
- }
- }
-
- class Solution {
- //每次你可以爬 1 或 2 个台阶, 进阶为每次可爬1-m个台阶
- public int climbStairs(int n) {
- int[] dp = new int[n+1];
- int m = 2;//设置可走步数
- if(n == 1 || n == 2) return n;
- dp[1] = 1;
- dp[2] = 2;
- for(int i = 3; i <= n; i++){
- for(int j = 1; j <= m && i >= j; j++){
- dp[i] += dp[i-j];
- }
- }
- return dp[n];
- }
-
-
-
- //746.达到楼梯顶部的最低花费
- class Solution {
- // cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用,在尾处加一个0表示楼顶,
- public int minCostClimbingStairs(int[] cost) {
- if(cost == null || cost.length <= 1) return 0;
- int[] dp = new int[cost.length+1];
- //可选择向上爬一个或者两个台阶
- dp[0] = cost[0];
- dp[1] = cost[1];
- for(int i = 2; i < cost.length+1; i++){
- if(i == cost.length){
- dp[i] = Math.min(dp[i-1],dp[i-2]) + 0;
- }else{
- dp[i] = Math.min(dp[i-1],dp[i-2]) + cost[i];//之前的最小消耗+当前需消耗值
- }
- }
- return dp[cost.length];
- }
- }
62. 不同路径:每次只能向下或者向右移动一步。机器人试图达到网格的右下角
63. 不同路径 II:考虑网格中有障碍物。从左上角到右下角将会有多少条不同的路
三种方法:
- // 动态规划
- class Solution {
- // 1.确定dp数组(dp table)以及下标的含义:dp[m][n]到达当前网格的路径数,下标:坐标
- // 2.确定递推公式:每次只能向下或者向右移动一步 dp[i][j] = dp[i-1][j] + dp[i][j-1]
- // 3.dp数组如何初始化:不能后退 dp[0][j] =1,dp[i][0]=1,到达机器人所在行列只有一种路径,直走
- // 4.确定遍历顺序 : 需一直i-1和j-1, 由上至下层次遍历
- // 5.举例推导dp数组,判断思路是否正确。
- public int uniquePaths(int m, int n) {
- int[][] dp = new int[m][n];
- for(int i = 0; i < m; i++) dp[i][0] = 1;
- for(int j = 0; j < n; j++) dp[0][j] = 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 {
- //网格中的障碍物和空位置分别用 1 和 0, 有障碍物则可到达该位置的路径为0
- //机器人每次只能向下或者向右移动一步,
- public int uniquePathsWithObstacles(int[][] obstacleGrid) {
- int m = obstacleGrid.length, n = obstacleGrid[0].length;
- int[][] dp = new int[m][n];
- if(obstacleGrid[m-1][n-1] == 1) return 0;
- //若第一行、列有障碍物,则行列中障碍物之后的格点均无法到达
- for(int i = 0; i< m && obstacleGrid[i][0] == 0; i++) dp[i][0] = 1;
- for(int j = 0; j < n && obstacleGrid[0][j] == 0; j++) dp[0][j] = 1;
- for(int i = 1; i< m; i++){
- for(int j = 1; j< n; j++){
- if(obstacleGrid[i][j] == 1){
- dp[i][j] = 0;
- }else{
- dp[i][j] = dp[i-1][j] + dp[i][j-1];
- }
- }
- }
- return dp[m-1][n-1];
- }
- }
-
- //数论方法 组合问题,到达终点的步数为:n-1+m-1,其中m-1步是向下走的,可形成的组合数,即路径数量
- class Solution {
- public int uniquePaths(int m, int n) {
- // int res = 1;
- // double numerator = n+m-2 ,denominator = m-1;//分子:A(n+m-2)(m-1),分母:A(m-1)(m-1)
- // for(int i = 0; i < m-1; i++){
- // res *= numerator/denominator;//会出现小数,损失精度了
- // numerator--;
- // denominator--;
- // }
- double numerator = 1 ,denominator = m-1;//分子:A(n+m-2)(m-1),分母:A(m-1)(m-1)
- for(int i = 0; i < m-1; i++){
- numerator *= (n+m-2-i);
- while(denominator > 0 && numerator % denominator == 0){//表示当前分子可整除分母,避免精度损失和栈溢出
- numerator = numerator/denominator;
- denominator--;
- }
- }
- return (int)numerator;
- }
- }
-
-
- // //递归,深度优先遍历,会超出时间限制
- // class Solution {
- // // 每次只能向下或者向右移动一步 , 即每个节点只有两个方向,将路径比作二叉树,寻找可到达终点即n+m-2层的叶子结点的路径个数
- // public int uniquePaths(int m, int n) {
- // return dfs(m-1,n-1,m,n);
- // }
- // public int dfs(int i, int j,int m, int n){
- // if(i == 0 || j == 0) return 1;//到达(i,j)节点的路径只有一个
- // return dfs(i-1,j,m,n) + dfs(i,j-1,m,n);
- // }
- // }
-
-
343. 整数拆分:一个正整数 n ,将其拆分为 k 个 正整数 的和,使所有整数的乘积最大化
96. 不同的二叉搜索树: 由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种
- class Solution {
- public int integerBreak(int n) {
- int[] dp = new int[n+1];//从0-n的所有最大拆分乘积
- dp[2] = 1;
- for(int i = 3; i< n+1;i++){ //计算从0-n的所有最大拆分乘积
- for(int j = i-1; j>= 1; j--){
- dp[i] = Math.max(j*(i-j),dp[i]);
- dp[i] = Math.max(j*dp[i-j],dp[i]);//与当前已知乘积比较,取最大值
- //拆分成两个j*(i-j)
- //拆分成多个数 j*dp[i-j]
- }
- }
- return dp[n];
- }
- }
-
- //96. 不同的二叉搜索树
- class Solution {
- // 二叉搜索树:左小右大
- // 有n节点,依次左右根节点,计算对应的二叉搜索树种类数,相加
- // 每个节点的二叉搜索树种类数 = 左子树的种类数*右子树的种类数 排列
- public int numTrees(int n) {
- 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++){ //根节点的值:节点值从 1 到 n 互不相同
- dp[i] += dp[j-1] * dp[i-j];
- }
- }
- return dp[n];
- }
- }
01背包:取与不取,每件物品只能用一次。物品在外由小到大、背包在内由大到小
完全背包:取几个(0-N),每件物品可用无数次。背包物品的遍历嵌套顺序均可,都是小到大
如果求组合数就是 外层for循环遍历物品,内层for遍历背包。
如果求排列数就是 外层for遍历背包,内层for循环遍历物品。
多重背包:不同物品数量不同。 1.将物品按数量展开成01背包,通过01背包2维遍历求解;2.物品在外由小到大、背包在内由大到小、物品数量由小到大。3维for遍历。
分组背包:按组进行选择,每组只能用一次
背包递推公式:
示例:
- 来自https://www.programmercarl.com/
- public static void main(String[] args) {
- int[] weight = {1, 3, 4};
- int[] value = {15, 20, 30};
- int bagsize = 4;
- testweightbagproblem(weight, value, bagsize);
- }
-
- //一维dp
- public static void testWeightBagProblem(int[] weight, int[] value, int bagWeight){
- int wLen = weight.length;
- //定义dp数组:dp[j]表示背包容量为j时,能获得的最大价值
- int[] dp = new int[bagWeight + 1];
- //遍历顺序:先遍历物品,再遍历背包容量
- for (int i = 0; i < wLen; i++){
- for (int j = bagWeight; j >= weight[i]; j--){
- dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
- }
- }
- }
-
- //二维dp
- public static void testweightbagproblem(int[] weight, int[] value, int bagsize){
- int wlen = weight.length, value0 = 0;
- //定义dp数组:dp[i][j]表示背包容量为j时,前i个物品能获得的最大价值
- int[][] dp = new int[wlen + 1][bagsize + 1];
- //初始化:背包容量为0时,能获得的价值都为0
- for (int i = 0; i <= wlen; i++){
- dp[i][0] = value0;
- }
- //遍历顺序:先遍历物品,再遍历背包容量
- for (int i = 1; i <= wlen; i++){
- for (int j = 1; j <= bagsize; j++){
- if (j < weight[i - 1]){
- dp[i][j] = dp[i - 1][j];
- }else{
- dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i - 1]] + value[i - 1]);
- }
- }
- }
- //打印dp数组
- for (int i = 0; i <= wlen; i++){
- for (int j = 0; j <= bagsize; j++){
- System.out.print(dp[i][j] + " ");
- }
- System.out.print("\n");
- }
- }
416. 分割等和子集:将数组分割成两个子集,使得两个子集的元素和相等。
1049. 最后一块石头的重量 II:对一组石头进行对消粉碎,返回此石头 最小的可能重量
- 416. 分割等和子集
- class Solution {
- public boolean canPartition(int[] nums) {
- int sum = 0;
- int target = 0;
- for(int i = 0; i< nums.length; i++){
- sum += nums[i];
- }
- if(sum % 2 != 0) return false;//和为奇数不可拆分
- target = sum/2;//分割成两个子集,两个子集的元素和相等
- // 0-1背包
- int[] dp = new int[target+1];
- dp[0] = 0;
- 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;
- }
- }
-
- 1049. 最后一块石头的重量 II
- class Solution {
- // 最小差值,最多只会剩下一块 石头
- // 不限制粉碎次数,则 只要将石头分成两份,使得两份的差值尽可能小,即越接近总重/2 差值越小
- public int lastStoneWeightII(int[] stones) {
- int sum = 0;
- for(int i = 0; i< stones.length; i++){
- sum += stones[i];
- }
- int target = sum/2;
- int[] dp = new int[target+1];
- dp[0] = 0;
- 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-dp[target]-dp[target];//sum-dp[target]表示另一集合的总重量
- }
- }

- 动态规划
- class Solution {
- //向数组中的每个整数前添加 '+' 或 '-',即分成两组,一组取正值,一组取负值
- // a-b = target a+b = sum ,a = (sum+target)/2
- //返回 运算结果等于 target 的不同 表达式 的数目。
- // 装满有几种方法: 组合问题 dp[j] += dp[j - nums[i]] dp[j]中存储的是装满j容量的方法数
- //当前和的组成方法数 = 放入当前值后,和-当前值 的组成方法数
- public int findTargetSumWays(int[] nums, int target) {
- int sum = 0;
- for(int i = 0; i< nums.length; i++){
- sum += nums[i];//nums[i]全》=0
- }
- if(sum < Math.abs(target)) return 0;//-1000 <= target <= 1000
- if( (sum+target) % 2 != 0) return 0;
- int[] dp = new int[(sum+target)/2+1]; //存放 当前容积有多少种存放方法
- dp[0] = 1;
- for(int i = 0; i< nums.length; i++){//依次不同大小的背包中放入物品
- for(int j = (sum+target)/2; j >= nums[i]; j--){
- dp[j] += dp[j-nums[i]];
- }
- }
- return dp[(sum+target)/2];
- }
- }
474. 一和零:找出并返回 二进制字符串数组 strs 的最大子集的长度,该子集中 最 有 m个 0和 n个 1 。
- class Solution {
- //该子集中 最多 有 m 个 0 和 n 个 1 。
- //找出并返回 strs 的最大子集的长度
- public int findMaxForm(String[] strs, int m, int n) {
- int[][] dp = new int[m+1][n+1];//各条件下最大子集的长度
- for(String s:strs){
- int zeronum = 0, onenum = 0;
- for(int i = 0; i< s.length(); i++){
- if(s.charAt(i) == '0') {
- zeronum ++;
- }else{
- onenum++;
- }
- }
- //01背包,每个元素:放与不放,不放原值dp[i][j],放1+dp[i-zeronum][j-onenum];
- for(int i = m; i >= zeronum; i--){
- for(int j = n; j >= onenum; j--){
- dp[i][j] = Math.max(dp[i][j], 1+dp[i-zeronum][j-onenum]);
- }
- }
- }
- return dp[m][n];
- }
- }
- // 先遍历物品,再遍历背包
- for(int i = 0; i < weight.size(); i++) { // 遍历物品
- for(int j = weight[i]; j <= bagWeight ; j++) { // 遍历背包容量
- dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
-
- }
- }
518. 零钱兑换 II: 返回可以凑成总金额的硬币组合数
377. 组合总和 Ⅳ:从 nums 中找出并返回总和为 target 的元素组合的个数。顺序不同的序列被视作不同的组合 == 排列数
322. 零钱兑换:最少的硬币个数 ,最少元素数
279. 完全平方数:返回 和为 n 的完全平方数的最少数量 ,最少元素数
139. 单词拆分:判断是否可以利用字典中出现的单词拼接出 s。判断是否可以装满背包
- 求组合数
- 518. 零钱兑换 II
- class Solution {
- public int change(int amount, int[] coins) {
- 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-coins[i]];
- }
- }
- return dp[amount];
- }
- }
-
-
- 求排列数
- 377. 组合总和 Ⅳ
- class Solution {
- //从 nums 中找出并返回总和为 target 的元素组合的个数。
- //顺序不同的序列被视作不同的组合。
- public int combinationSum4(int[] nums, int target) {
- int[] dp = new int[target+1];
- dp[0] = 1;//和为0 的组合数:1
- for(int i= 0; i <= target; i++){
- for(int j = 0; j < nums.length; j++){
- if(i >= nums[j]){
- dp[i] += dp[i-nums[j]];
- }
- }
- }
- return dp[target];
- }
- }
-
- 322. 零钱兑换:最少的硬币个数
- class Solution {
- //计算并返回可以凑成总金额所需的 最少的硬币个数, 无则返回-1
- //每种硬币的数量是无限的。 完全背包, 元素数量问题:组合排列都可以
- public int coinChange(int[] coins, int amount) {
- if(coins.length == 1 && amount % coins[0] != 0) return -1;//无法构成
- int[] dp = new int[amount+1];// 存放凑成各金额所需的 最少的硬币个数
- Arrays.fill(dp,Integer.MAX_VALUE);
- dp[0] = 0;
- 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-coins[i]]+1,dp[j]);
- }
- }
- }
- return dp[amount] == Integer.MAX_VALUE? -1: dp[amount];
- }
- }
-
-
- 279. 完全平方数: 求元素最少个数
- class Solution {
- //返回 和为 n 的完全平方数的最少数量 。
- //完全平方数 是一个整数,其值等于另一个整数的平方,eg:1、4、9 和 16
- //可重复选取:完全背包
- public int numSquares(int n) {
- int[] dp = new int[n+1]; //组成和为n的完全平方数的最小数量
- Arrays.fill(dp, Integer.MAX_VALUE);
- dp[0] = 0;
- //先背包,后物品
- // for(int i = 1; i <= n; i++){
- // for(int j = 1; j*j <= i;j++){
- // if(dp[i-j*j] != Integer.MAX_VALUE){
- // dp[i] = Math.min(dp[i], dp[i-j*j]+1);
- // }
- // }
- // }
-
- // 先物品,后背包
- for(int i = 0; i <= Math.sqrt(n); i++){
- for(int j = i*i; j <= n; j++){
- if(dp[j - i*i] != Integer.MAX_VALUE){
- dp[j] = Math.min(dp[j - i*i] + 1, dp[j]);
- }
- }
- }
- return dp[n] == Integer.MAX_VALUE? -1:dp[n];
- }
- }
-
-
- 139. 单词拆分:s是否可以装满背包
- // class Solution {
- // //不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。 即:完全背包
- // //判断是否可以利用字典wordDict中出现的单词拼接出 s
- // //s:背包 wordDict:物品
- // //递推公式是 if([j, i] 这个区间的子串出现在字典里 && dp[j]是true) 那么 dp[i] = true。
- // public boolean wordBreak(String s, List
wordDict) { - // boolean[] dp = new boolean[s.length()+1];//存放,当前位置之前的全部元素是否已可由字典组成。
- // dp[0] = true;//0下标表示没有元素null
- // for(int i = 1; i <= s.length(); i++){
- // for(int j = 0; j < i; j++){//截取字符串,判断是否存在字典中
- // String str = s.substring(j,i);
- // if(wordDict.contains(str) && dp[j] == true){
- // dp[i] = true;
- // }
- // }
- // }
- // return dp[s.length()];
- // }
- // }
-
- // 回溯法+记忆化
- class Solution {
- int[] flag ;
- public boolean wordBreak(String s, List
wordDict) { - flag = new int[s.length()];
- return backtracking(s,0,wordDict);
- }
-
- public boolean backtracking(String s, int index, List
wordDict) { - if(index == s.length()){
- return true;//已经寻找结束,前面的已完全匹配
- }
- if(flag[index] == -1){
- return false;//flag中元素表示,从index开始向后匹配能否匹配成功,不能则直接结束
- }
- for(int i = index; i < s.length(); i++){
- String str = s.substring(index,i+1);//截取子字符串,左闭右开
- if(wordDict.contains(str)){
- boolean res = backtracking(s, i+1, wordDict);
- if(res) return true;
- }
- }
- //到这,表示以改坐标为起点,匹配不成功,标记并返回
- flag[index] = -1;
- return false;
- }
-
- }
198. 打家劫舍:队列排列,两间相邻的房屋不能同时进入,求可偷窃到的最高金额。 Math.max(dp[i-2]+nums[i], dp[i-1]),偷、不偷,取较大值
213. 打家劫舍 II:环状排列,两间相邻的房屋不能同时进入+首尾房间紧挨着。首尾不能同时可选,分两种情况:头可选、尾可选 ,分别计算 取两者的较大值
337. 打家劫舍 III:二叉树排列,一个入口,每个房子有且仅有一个父房子,两间直接相连的房屋不能同时进入。树状结构:后序遍历 + 二维数组标记左右节点状态与值,根据父节点状态取值
- 198. 打家劫舍
- class Solution {
- //不可取 两间相邻的元素,所以每次至少走2步
- //偷窃到的最高金额。
- public int rob(int[] nums) {
- if(nums.length == 1) return nums[0];
- int[] dp = new int[nums.length];//存放总金额
- dp[0] = nums[0];//偷时金额为0
- dp[1] = Math.max(nums[0],nums[1]);
- //影响dp[i]的因素是,i房屋偷不偷,
- //偷dp[i] = nums[i]+dp[i-2],i+1不可偷;不偷,dp[i] = dp[i-1],i+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];
- }
- }
-
- 213. 打家劫舍 II
- class Solution {
- //所有的房屋都 围成一圈 第一个房屋和最后一个房屋是紧挨着的:不能同时可取 分两种情况考虑
- //两间相邻的房屋不能同时进入
- //求偷窃到的最高金额 dp
- public int rob(int[] nums) {
- if(nums.length == 1) return nums[0];
- int res1 = robRange(nums, 0, nums.length-1);//考虑头元素,不取尾
- int res2 = robRange(nums, 1, nums.length);//考虑尾元素 ,不取头
- return Math.max(res1, res2);
- }
-
- 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-2]+nums[i], dp[i-1]);//第i个房间偷不偷
- }
- return dp[end-1];
- }
- }
-
-
- 337. 打家劫舍 III
- //从叶子结点开始遍历,后序遍历
- // 当前节点i 偷不偷,偷,i-2层的数组和,左左,左右,右左,右右+value, 不偷,dp[i] = dp[i].left+dp[i].right
- class Solution {
- Map
map = new HashMap<>();//记录每个节点的最大金额,没有记录每个节点的状态(偷没偷) - public int rob(TreeNode root) {
- if(root == null) return 0;
- if(map.containsKey(root)) return map.get(root);
- //偷root节点
- int res1 = root.val;
- if(root.left != null){
- res1 += rob(root.left.left) + rob(root.left.right);
- }
- if(root.right != null){
- res1 += rob(root.right.left) + rob(root.right.right);
- }
-
- //不偷root节点
- int res2 = rob(root.left) + rob(root.right);
- int val = Math.max(res1,res2);
- map.put(root,val);
- return val;
- }
- }
- // 记录每个节点的状态(偷 / 没偷)以及对应最大金额,返回值为int[2]类型;
- // 不偷:Max(左孩子不偷,左孩子偷) + Max(又孩子不偷,右孩子偷)
- // root[0] = Math.max(rob(root.left)[0], rob(root.left)[1]) +
- // Math.max(rob(root.right)[0], rob(root.right)[1])
- // 偷:左孩子不偷+ 右孩子不偷 + 当前节点偷
- // root[1] = rob(root.left)[0] + rob(root.right)[0] + root.val;
- class Solution {
- public int rob(TreeNode root) {
- int[] res = robTree(root);
- return Math.max(res[0], res[1]);//0没偷,1偷了
- }
- public int[] robTree(TreeNode root){
- if(root == null) return new int[]{0,0};//相当于初始化
- int[] res = new int[2];
- int[] left = robTree(root.left);
- int[] right = robTree(root.right);//不要在下面的表达式中直接写,会多次重复运算
- res[0] = 0 + Math.max(left[0], left[1])
- + Math.max(right[0], right[1]);
- res[1] = root.val +left[0] +right[0];
- return res;
- }
- }
121. 买卖股票的最佳时机:只能选择某天买进,之后的某天卖出,取最大利润
股票全程只能买卖一次:(贪心:左最低买入,右最高卖出)两种状态
股票全程能买卖多次:(贪心:取所有正收益的和)两种状态:延续,改变状态
股票最多可以买卖两次:5种状态
股票最多可以买卖K次:2*k+1种状态,即0:无操作,奇数:买入 -prices,偶数:卖出 +prices
309. 最佳买卖股票时机含冷冻期:买入时 跳过冷冻期时间取之前卖出收益
714. 买卖股票的最佳时机含手续费:买入时有手续费:买入时 当前收益-股价-手续费
- 121. 买卖股票的最佳时机
- class Solution {
- //贪心算法,取最小买入价格,最高收益,效率更高
- public int maxProfit(int[] prices) {
- int min = Integer.MAX_VALUE;
- int res = 0;//记录最大利润
- for(int i = 0; i< prices.length; i++){
- min = Math.min(min,prices[i]);//取最低买入金额
- res = Math.max(res,prices[i] - min);//之前卖出,和 当前卖出 取较大值
- }
- return res;
- }
- }
-
- class Solution {
- //动态规划,每日更新
- public int maxProfit(int[] prices) {
- int[] dp = new int[2];//0:持有(当天买入、之前买入) ,1:不持有(卖出,之前卖出)
- //dp存放获得最大收益
- dp[0] = -prices[0];
- dp[1] = 0;
- for(int i = 1; i < prices.length; i++){
- dp[0] = Math.max(dp[0],-prices[i]);
- dp[1] = Math.max(dp[1], prices[i]+dp[0]);//当天卖出,前一天一定是持有的
- }
- return dp[1];//卖出,收益更高
- }
- }
-
-
- 122. 买卖股票的最佳时机 II
- class Solution {
- public int maxProfit(int[] prices) {
- int len = prices.length;
- int[][] dp = new int[len][2];//0持有,1不持有
- dp[0][0] = -prices[0];
- dp[0][1] = 0;
- for(int i = 1; i< len; i++){
- dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1]-prices[i]);
- dp[i][1] = Math.max(prices[i]+dp[i-1][0], dp[i-1][1]);
- }
- return dp[len-1][1];
-
- }
- }
-
- 123. 买卖股票的最佳时机 III
- class Solution {
- //你最多可以完成 两笔 交易。
- public int maxProfit(int[] prices) {
- int[] dp = new int[5];//有5种状态下的收益
- dp[0] = 0;//无操作
- dp[1] = -prices[0];//第一次买入
- dp[2] = 0;//第一次卖出
- dp[3] = -prices[0];//第二次买入
- dp[4] = 0;//第二次卖出
- for(int i = 1; i < prices.length; i++){
- //后面的状态需要前面的值,从4-》0更新状态收益
- dp[4] = Math.max(dp[4], dp[3]+prices[i]);//延续,或当天改变状态
- dp[3] = Math.max(dp[3], dp[2]-prices[i]);
- dp[2] = Math.max(dp[2], dp[1]+prices[i]);
- dp[1] = Math.max(dp[1], dp[0]-prices[i]);
- dp[0] = 0;
- }
- return dp[4];//最后的卖出状态收益更高
- }
- }
-
- 188. 买卖股票的最佳时机 IV:最多可以完成 k 笔交易。
- class Solution {
- public int maxProfit(int k, int[] prices) {
- if(prices == null || prices.length == 0) return 0;
- int[] dp = new int[2*k+1];
- dp[0] = 0;
- for(int j = 1; j < 2*k+1; j = j+2){
- dp[j] = -prices[0];
- }
- for(int i = 1; i < prices.length; i++){
- for(int j = 2*k; j > 0; j--){
- int sign = (j%2==0? 1:-1);//奇数买入,偶数卖出
- dp[j] = Math.max(dp[j],dp[j-1]+sign*prices[i]);
- }
- }
- return dp[2*k];
- }
- }
-
-
- 309.最佳买卖股票时机含冷冻期
- class Solution {
- //卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
- //尽可能地完成更多的交易(多次买卖一支股票)
- public int maxProfit(int[] prices) {
- if(prices == null || prices.length <= 1) return 0;
- //状态:0买入(第i天买入的话之前的收益为i-2天的卖出状态的收益),1卖出
- int[][] dp = new int[prices.length][2];
- dp[0][0] = -prices[0];
- dp[0][1] = 0;
- dp[1][0] = Math.max(dp[0][0],-prices[1]);
- dp[1][1] = Math.max(prices[1]+dp[0][0],0);
- for(int i = 2; i < prices.length; i++){
- dp[i][0] = Math.max(dp[i-1][0], dp[i-2][1]-prices[i]);//买入状态
- dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0]+prices[i]);//卖出状态
- }
- return dp[prices.length-1][1];
- }
- }
-
- 714. 买卖股票的最佳时机含手续费
- class Solution {
- public int maxProfit(int[] prices, int fee) {
- // int[][] dp = new int[prices.length][2];//0买入,1卖出
- // dp[0][0] = -prices[0]-fee;
- // dp[0][1] = 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][1];
- int[] dp = new int[2];//0买入,1卖出
- dp[0] = -prices[0]-fee;
- dp[1] = 0;
- for(int i = 1; i< prices.length; i++){
- dp[0] = Math.max(dp[0], dp[1]-prices[i]-fee);
- dp[1] = Math.max(dp[1], dp[0]+prices[i]);
- }
- return dp[1];
- }
- }

300. 最长严格递增子序列:j = 0~i nums[i] > nums[j]则 dp[i] = max(dp[i], dp[j] + 1);即长度+1674. 最长连续递增序列:nums[i]>nums[i-1] 则 dp[i] = Math.max(dp[i],dp[i-1]+1)
53. 最大连续子数组和: 贪心(只取连续子列的正和,否则为0以当前元素从新开始)、动态规划(求当前位置i的最大连续子数组和:dp[i] = Math.max(dp[i-1]+nums[i], nums[i]) 取所有dp的最大值)
- 300. 最长递增子序列
- class Solution {
- public int lengthOfLIS(int[] nums) {
- if(nums == null || nums.length == 0) return 0;
- int[] dp = new int[nums.length];//记录以nums[i]为结尾的子序列的最长递增子序列的长度
- int res = 1;
- Arrays.fill(dp,1);//递增子序列的最小长度为1
- 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);//当前数值为结尾的递增子序列的最大长度+1
- }
- }
- res = Math.max(dp[i],res);//取最大值
- }
- return res;
- }
- }
-
-
- 674. 最长连续递增序列
- class Solution {
- //最长且 连续递增的子序列
- public int findLengthOfLCIS(int[] nums) {
- //动态规划
- // int[] dp = new int[nums.length];
- // int res = 1;
- // Arrays.fill(dp,1);
- // for(int i = 1; i< nums.length; i++){
- // if(nums[i]>nums[i-1]){//连续
- // dp[i] = Math.max(dp[i],dp[i-1]+1);//存储到当前值的最大长度
- // }
- // res = Math.max(dp[i],res);
- // }
- // return res;
-
- //贪心算法,更快
- int count = 1; //记录递增长度
- int res = 1; //记录最大长度
- for(int i = 1; i< nums.length; i++){
- if(nums[i]>nums[i-1]){
- count++;
- }else{
- count = 1;
- }
- res = Math.max(res,count);
- }
- return res;
- }
- }
-
-
-
- 53. 最大子数组和
- class Solution {
- public int maxSubArray(int[] nums) {
- // 贪心算法,支取正和区间
- // if(nums.length == 1) return nums[0];
- // int res = Integer.MIN_VALUE;
- // int sum = 0;
- // for(int i= 0; i< nums.length; i++){
- // if(sum < 0){
- // sum = 0;//之前元素的和为负数,则不取
- // }
- // sum += nums[i];
- // res = Math.max(sum,res);
- // }
- // return res;
-
- //动态规划 dp[i] 0-i的子序列的最大和
- int[] dp = new int[nums.length];
- dp[0] = nums[0];
- int res = dp[0];
- for(int i=1; i< nums.length; i++){
- dp[i] = Math.max(dp[i-1]+nums[i], nums[i]);//看之前取的元素和是正还是负
- res = res > dp[i]? res: dp[i];//取所有最大和的最大值
- }
- return res;
- }
- }
-
重点:1. dp矩阵的定义,2. 当前元素存在的几种情况和相应的操作 对dp元素值的影响
718. 最长重复子数组: 求两个数组的最大重复子序列长度,二维遍历比较,相同+1。
1143. 最长公共子序列: 求两个字符串的 最大公共子序列长度(即相对位置保持一致),二维遍历,相同则+1(dp[i-1][j-1]+1),不同取相邻位置的较大值max(dp[i-1][j],dp[i][j-1]).
1035. 不相交的线: 连接两个数组中的相同元素,保证连线之间不相交,求绘制的最大连线数 = 元素的相对位置保持一致,求最大公共子序列长度。与上题一致。
392. 判断子序列: 判断 s 是否为 t 的子序列, 即s和t的最大公共子序列的长度是否等于s的长度。(直接向后查询所需元素,或者双指针法比较元素 均可解出)
- if (s[i - 1] == t[j - 1]){
- dp[i][j] = dp[i - 1][j - 1] + 1; //t中找到了一个字符在s中也出现了
-
- }else {
- dp[i][j] = dp[i][j - 1];//相当于t要删除元素,继续匹配
- }
115. 不同的子序列:计算在 s 的子序列中 t 出现的个数。(元素的相对位置相同)。
判断当前元素是否对应相等,相等叠加;,不等保持 ;
- if (s[i - 1] == t[j - 1]) {
- dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
- //相等 时 = 当前包含s[i-1]元素时+ 不包含s[i-1]元素时 t(0-j-1子序列)的出现个数
- } else {
- dp[i][j] = dp[i - 1][j];
- //不相等时 = 不包含s[i-1]元素时 t(0-j-1子序列)的出现个数
- }
583. 两个字符串的删除操作: 两个字符串只能删除元素,使得 二者相同所需的最小步数。
对应元素相等,操作数不增加; 不相等,删除w1元素,对应位置操作数的基础上+1,删除w2元素,操作数+1;l两者都删除,操作数+2;
72. 编辑距离: 对字符串进行插/删除/替换字符操作,变成目标字符串,最少需要多少操作数。
- dp[i][j]:表示s1的0~i-1和s2的0~j-1的编辑距离
- if (word1[i - 1] == word2[j - 1])
- dp[i][j] = dp[i - 1][j - 1];//不操作
- if (word1[i - 1] != word2[j - 1])
- dp[i][j] = dp[i - 1][j] + 1;//增
- dp[i][j] = dp[i][j - 1] + 1;//删
- dp[i][j] = dp[i - 1][j - 1] + 1;//换
- //三种情况中选一个
- //题目要求:最小操作数:取三者最小值
- 718. 最长重复子数组
- class Solution {
- public int findLength(int[] nums1, int[] nums2) {
- // int[][] dp = new int[nums1.length+1][nums2.length+1];
- // //dp[i][j] 表示nums1 0~i-1处 与nums2 0~j-1处的公共序列长度
- // //dp[i][0] = 0; dp[0][j] = 0;均表示未取值
- // int res = 0;
- // for(int i = 0; i< nums1.length; i++){
- // for(int j = 0; j< nums2.length; j++){
- // if(nums1[i] == nums2[j]){
- // dp[i+1][j+1] = dp[i][j]+1;
- // res = Math.max(dp[i+1][j+1],res);//保存最大值
- // }
- // }
- // }
- // return res;
-
- //将二维变为一维, 向下对角线上元素相等+1
- int[] dp = new int[nums2.length+1];
- int res = 0;
- //类似背包
- for(int i = 0; i < nums1.length; i++){//物品
- for(int j = nums2.length-1; j>=0; j--){//背包 从后向前遍历,避免重复
- if(nums1[i] == nums2[j]){
- dp[j+1] = dp[j]+1;
- }else{
- dp[j+1] = 0;//当前数值不相等,表示以当前数值为结尾的序列是不存在公共序列的
- }
- res = Math.max(dp[j+1],res);
- }
- }
- return res;
- }
- }
-
-
-
-
- 1143. 最长公共子序列
- class Solution {
- public int longestCommonSubsequence(String text1, String text2) {
- if(text1 == null || text2 == null || text1.length() == 0|| text2.length() == 0){
- return 0;
- }
- char[] t1 = text1.toCharArray();
- char[] t2 = text2.toCharArray();
-
- //二维dp
- int[][] dp = new int[t1.length+1][t2.length+1];
- //dp[i][j] : text1的0~i-1的子序列与 text2的0~j-1的子序列 的最大公共子序列的长度
- //第0行、0列均为0, 列text2元素,行text1元素
- for(int i = 1; i <= t1.length; i++){
- for(int j = 1; j<= t2.length; j++){
- if(t1[i-1] == t2[j-1]){
- dp[i][j] = dp[i-1][j-1]+1;//相等+1
- }else{
- dp[i][j]= Math.max(dp[i-1][j],dp[i][j-1]);//不等取最大值
- }
-
- }
- }
- return dp[t1.length][t2.length];
- }
- }
- }
-
-
-
- 392. 判断子序列:判断 s 是否为 t 的子序列。
- class Solution {
- public boolean isSubsequence(String s, String t) {
- if(s == null || t == null || s.length() > t.length()){
- return false;
- }//特殊情况判断
- int index = -1;
- for(char c : s.toCharArray()){
- index = t.indexOf(c,index+1);
- if(index == -1) return false;//没找到该元素
- }
- return true;
-
- //双指针法
- int si = 0;
- int ti = 0;
- while(si < s.length() && ti < t.length()){
- if(s.charAt(si) == t.charAt(ti)){
- si++;
- }//当前元素以匹配,后移指针
- ti++;
- }
- return si == s.length();
-
- //动态规划:s 为 t 的子序列即 s/t最大公共子序列的长度为s的长度
- int[][] dp = new int[s.length()+1][t.length()+1];
- //dp[i][j] : 表示s的0~i-1 与t的0~j-1的相同子序列的长度
- char[] s1 = s.toCharArray();
- char[] t1 = t.toCharArray();
- for(int i = 1; i <= s.length(); i++){
- for(int j = 1; j <= t.length(); j++){
- if(s1[i-1] == t1[j-1]){
- dp[i][j] = dp[i-1][j-1] + 1;
- }else{
- dp[i][j] = dp[i][j-1];//dp[i-1][j] 一定比dp[i][j-1]小,因为 t1[j-1]无用
- }
- }
- }
- return dp[s.length()][t.length()] == s.length();//最大公共子序列的长度为s的长度
-
- }
- }
-
-
- 115. 不同的子序列:计算在 s 的子序列中 t 出现的个数。(元素的相对位置相同)
- class Solution {
- public int numDistinct(String s, String t) {
- int[][] dp = new int[s.length()+1][t.length()+1];
- //dp:在 s 的子序列中 t 出现的个数。dp[0][j] = 0,s为[],不可能包含t
- for(int i = 0; i < s.length(); i++){
- dp[i][0] = 1;//即t为空[]
- }
- char[] s1 = s.toCharArray();
- char[] t1 = t.toCharArray();
- for(int i = 1; i<= s.length(); i++){
- for(int j = 1; j <= t.length(); j++){
- //延续 或 叠加
- if(s1[i-1] == t1[j-1]){
- //相等 = 当前包含s[i-1]元素时+ 不包含s[i-1]元素时 t(0-j-1子序列)的出现个数
- dp[i][j] = dp[i-1][j-1] + dp[i-1][j];
- }else{
- dp[i][j] = dp[i-1][j];//不包含s[i-1]元素时 t(0-j-1子序列)的出现个数
- }
- }
- }
- return dp[s.length()][t.length()];
- }
- }
-
-
- 72. 编辑距离
- class Solution {
- public int minDistance(String word1, String word2) {
- int l1 = word1.length();
- int l2 = word2.length();
- int[][] dp = new int[l1+1][l2+1];
- char[] w1 = word1.toCharArray();
- char[] w2 = word2.toCharArray();
- //初始化,字符串《-》空集 的操作数 = 元素数
- for(int i = 0; i <= l1; i++){
- dp[i][0] = i;
- }
- for(int j = 0; j <= l2; j++){
- dp[0][j] = j;
- }
- for(int i = 1; i <= l1; i++){
- for(int j = 1; j <= l2; j++){
- if(w1[i-1] == w2[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;
- //dp[i-1][j]+1 :w2添加元素 即添加w1[i-1]元素
- //dp[i][j-1]+1 : W2删除w2[j-1]元素
- //dp[i-1][j-1]+1 :w2替换
- }
- }
- }
- return dp[l1][l2];
- }
- }
-
- 583. 两个字符串的删除操作
- class Solution {
- public int minDistance(String word1, String word2) {
- int l1 = word1.length();
- int l2 = word2.length();
- char[] w1 = word1.toCharArray();
- char[] w2 = word2.toCharArray();
- //方式一:求两个字符串的公共序列长度,再用各自字符串长度- 公共序列长度
- //方式二:直接统计使得当前子序列相等 的最小步数
- int[][] dp = new int[l1+1][l2+1];//子序列相等 的最小步数
- // 要与空集相等的最小步数 = 序列长度
- for(int i = 0; i <= l1; i++){
- dp[i][0] = i;
- }
- for(int j = 1; j <= l2; j++){
- dp[0][j] = j;
- }
- for(int i = 1; i <= l1; i++){
- for(int j = 1; j <= l2; j++){
- if(w1[i-1] == w2[j-1]){//相等,不操作
- dp[i][j] = dp[i-1][j-1] ;
- }else{//不相等,取当前最小步数
- dp[i][j] = Math.min(Math.min(dp[i-1][j]+1,dp[i][j-1]+1),dp[i-1][j-1]+2);
- //删除w1[i-1];删除w2[j-1];两个都删除;
- }
- }
- }
- return dp[l1][l2];
- }
- }
647. 回文子串:统计并返回这个字符串中 回文子串 的数目,统计dp中为true的单元数
- if (s[i] == s[j]) {
- if (j - i <= 1) { // 情况一 和 情况二
- //下标相同或者相差为1,该字符串为回文子串
- result++;
- dp[i][j] = true;
- } else if (dp[i + 1][j - 1]) { // 情况三
- //相差> 1,需要判断他们的中间字符串是否是回文的
- result++;
- dp[i][j] = true;
- //当前结果需要已知dp[i + 1][j - 1],遍历顺序i:下到上,j:左到右
- }
- }
516. 最长回文子序列: 找出其中最长的回文子序列按顺序选择的字符串)
dp[i][j]: 字符串i-j位置的子序列构成的回文子串的最大长度;;相等:数量+2,不等,取可能情况的最大值
- 647. 回文子串
- //动态规划
- class Solution {
- public int countSubstrings(String s) {
- int len = s.length();
- char[] s1 = s.toCharArray();
- boolean[][] dp = new boolean[len][len];//初始值false
- int count = 0;
- for(int i = len-1; i >= 0; i--){
- for(int j = i; j < len; j++){
- if(s1[i] == s1[j]){
- if(j-i <= 1 || dp[i+1][j-1] == true){
- dp[i][j] = true;
- count++;
- }
- }
- }
- }
- return count;
- }
- }
-
- //双指针法,以对称中心(1个元素,2个元素)向两侧出发,比较对应元素
- class Solution {
- public int countSubstrings(String s) {
- int count = 0;
- char[] s1 = s.toCharArray();
- for(int i = 0; i< s1.length; i++){
- count += extend(i,i,s1);
- count += extend(i,i+1,s1);
- }
- return count;
- }
- public int extend(int i, int j, char[] s){
- int res = 0;
- while(i >= 0 && j < s.length && s[i] == s[j]){//考虑边界
- i--;
- j++;
- res++;
- }
- return res;
- }
- }
-
- 516. 最长回文子序列: 不改变剩余字符顺序的情况下,删除某些字符
- class Solution {
- public int longestPalindromeSubseq(String s) {
- int len = s.length();
- char[] s1 = s.toCharArray();
- int[][] dp = new int[len][len];//i-j的回文子序列的最长长度
- int res = 0;
- if(s == null || len == 0){
- return res;
- }else{
- res = 1;
- }
- for(int i = 0; i < len; i++){
- dp[i][i] = 1;//单个元素也是回文
- }
- for(int i = len-1; i >= 0; i--){
- for(int j = i+1; j < len; j++){
- if(s1[i] == s1[j]){
- dp[i][j] = dp[i+1][j-1]+2;//i 大到小,j小到大
- }else{
- dp[i][j] = Math.max(Math.max(dp[i+1][j],dp[i][j-1]),dp[i+1][j-1]);
- //不等时,删除左侧元素、还是右侧元素,还是两边都删除
- }
- res = Math.max(res, dp[i][j]);
- }
- }
- return res;
- }
- }