动态规划【代码随想录|Java|刷题记录】

1.斐波那契

class Solution {
    public int fib(int n) {
        if(n<=1){   //注意特判
            return n;
        }
        int[] f = new int[n+1]; //注意这里是n+1
        f[0]=0;
        f[1]=1;
        for(int i=2;i<=n;i++){
            f[i]=f[i-1]+f[i-2];
        }
        return f[n];
    }
}

2.爬楼梯

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

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

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

class Solution {
    public int climbStairs(int n) {
        if(n<=2){
            return n;
        }
        //第n层可以从n-1或者n-2层得来
        int[] step = new int[n+1];
        step[1]=1;
        step[2]=2;
        for(int i=3;i<=n;i++){
            step[i]=step[i-1]+step[i-2];
        }
        return step[n];
    }
}

3.最小花费爬楼梯

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

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

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

class Solution {
    public int minCostClimbingStairs(int[] cost) {
        int height = cost.length;
        int[] sum = new int[height + 1];

        // 初始化前两个台阶的最小花费
        sum[0] = 0;
        sum[1] = 0;
        //注意下标为0代表地面
        //而且楼梯顶部是没有cost的 所以要返回的是sum[height]
        for (int i = 2; i <= height; i++) {
            sum[i] = Math.min(sum[i - 1] + cost[i - 1], sum[i - 2] + cost[i - 2]);
        }

        return sum[height];
    }
}

4.不同路径

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

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

问总共有多少条不同的路径?

class Solution {
    public int uniquePaths(int m, int n) {
        int[][] step = new int[m][n];
        //注意问的是路径数,不是步数
        //初始化第一行和第一列
        for(int i=0;i<m;i++){
            step[i][0]=1;
        }
        for(int i=0;i<n;i++){
            step[0][i]=1;
        }
        //计算所有路径数
        for(int i=1;i<m;i++){
            for(int j=1;j<n;j++){
                step[i][j] = step[i - 1][j] + step[i][j - 1];
            }
        }
        return step[m-1][n-1];
    }
}

5.不同路径2

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

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

现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?

class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int m =obstacleGrid.length;
        int n=obstacleGrid[0].length;
        int[][] step = new int[m][n];
        //起点有障碍物,则直接返回0
        if (obstacleGrid[0][0] == 1||obstacleGrid[m-1][n-1]==1) {
            return 0;
        }
        //初始化第一行和第一列
        step[0][0]=1;

       for (int i = 0; i < m && obstacleGrid[i][0] == 0; i++) {
            step[i][0] = 1;//一旦遇到障碍物就停止遍历了
        }
        for (int j = 0; j < n && obstacleGrid[0][j] == 0; j++) {
            step[0][j] = 1;
        }


        //计算所有路径数
        for(int i=1;i<m;i++){
            for(int j=1;j<n;j++){
                if(obstacleGrid[i][j]==1){
                    step[i][j]=0;//这一行 如果这一列是障碍物 说明不能向右走 要去下一行了
                    continue;
                }
                step[i][j] = step[i - 1][j] + step[i][j - 1];
            }
        }
        return step[m-1][n-1];
    }
}

6.整数拆分

给定一个正整数 n ,将其拆分为 k正整数 的和( k >= 2 ),并使这些整数的乘积最大化。

返回 你可以获得的最大乘积

class Solution {
    public int integerBreak(int n) {
        int[] dp = new int[n+1]; // dp[i]就是数字i拆分后的最大乘积
        dp[2]=1; 
        for(int i=3;i<=n;i++){
            for(int j=1;j<=i-j;j++){ //因为后面是用的j*(i-j) 就可以直接相乘 再往后遍历就重复了
                dp[i]=Math.max(dp[i],Math.max(j*(i-j),j*dp[i-j]));
                //外层的max是为了遍历每一个j 取最大值
                //内层的max是为了决定当前的j是否要继续拆分
            }
        }
        return dp[n];
    }
}

##7.不同的二叉搜索树

给定一个整数 n,求以 1 … n 为节点组成的二叉搜索树有多少种?

class Solution {
    public int numTrees(int n) {
        int[] dp = new int[n+1];
        dp[0]=1;
        dp[1]=1;
        for(int i=2;i<=n;i++){
            //外层是要通过递推 从1到n为根节点的情况
            for(int j=1;j<=i;j++){
                 //对于第i个节点,需要考虑1作为根节点直到i作为根节点的情况,所以需要累加
                //一共i个节点,对于根节点j时,左子树的节点个数为j-1,右子树的节点个数为i-
                dp[i]+=dp[j-1]*dp[i-j];
            }
        }
        return dp[n];
    }
}

8. 01背包

有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大

  • dp数组含义

dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少

  • 递推公式

    • 不放物品i:背包容量为j,里面不放物品i的最大价值是dp[i - 1][j]
    • 放物品i:背包空出物品i的容量后,背包容量为j - weight[i],dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]且不放物品i的最大价值,那么dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值

    递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

  • 初始化

    • 如果背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为0。

    • 状态转移方程 dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 可以看出i 是由 i-1 推导出来,那么i为0的时候就一定要初始化。

      dp[0][j],即:i为0,存放编号0的物品的时候,各个容量的背包所能存放的最大价值。

      那么很明显当 j < weight[0]的时候,dp[0][j] 应该是 0,因为背包容量比编号0的物品重量还小。【可以不用处理】

      j >= weight[0]时,dp[0][j] 应该是value[0],因为背包容量放足够放编号0物品。

    • dp[0][j]dp[i][0] 都已经初始化了,那么其他下标应该初始化多少呢?

      • 其实从递归公式可知,dp[i][j] 是由左上方数值推导出来了,那么 其他下标初始为什么数值都可以,因为都会被覆盖。
      • 但只不过一开始就统一把dp数组统一初始为0,更方便一些。
import java.util.Scanner;

public class Main{
    public static void main(String[] args){
        Scanner sc= new Scanner(System.in);
        int m = sc.nextInt();
        int n = sc.nextInt();

        int weight[] = new int[m];
        int val[]=new int[m];
        int dp[][]=new int[m][n+1];

        for (int i = 0; i < m; ++i) {
            weight[i] = sc.nextInt();
        }
        for (int j = 0; j < m; ++j) {
            val[j] = sc.nextInt();
        } 

        //初始化
        for(int i=weight[0];i<=n;i++){
                dp[0][i]=val[0];
        }
        //3.先遍历物品再遍历背包
        for(int i=1;i<m;i++){   //i从1开始,因为i=0的时候已经初始化了
            for(int j=0;j<=n;j++){ //注意书包容量是0到n
                if(weight[i]<=j){
                    dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-weight[i]]+val[i]);
                }else{
                    dp[i][j]=dp[i-1][j];
                }
            }
        }
         System.out.println(dp[m - 1][n]);
    }
}

##9.01背包的滚动数组

  • 一维dp数组(滚动数组)

对于背包问题其实状态都是可以压缩的。

在使用二维数组的时候,递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

其实可以发现如果把dp[i - 1]那一层拷贝到dp[i]上,表达式完全可以是:dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);

与其把dp[i - 1]这一层拷贝到dp[i]上,不如只用一个一维数组了,只用dp[j](一维数组,也可以理解是一个滚动数组)。

  • dp数组定义

    • dp[j]为 容量为j的背包所背的最大价值
  • 递推公式

    • 一维dp数组,其实就由上一层 dp[i-1] 这一层 拷贝的 dp[i]来。
    • 所以在 上面递推公式的基础上,去掉i这个维度就好。
    • 递推公式为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
  • 初始化:都初始化为0

  • 遍历顺序:

    • for(int i = 0; i < weight.size(); i++) { // 遍历物品
          for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
              dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
          }
      }
      
    • 为什么滚动数组(一维 DP)是倒序遍历的?

      1. 先来看正序遍历的错误

      假设有一个 dp 数组,dp[j] 代表容量为 j 时的最大价值。

      • 物品 0 的重量 weight[0] = 1,价值 value[0] = 15
      • 初始 dp = [0, 0, 0](假设背包容量为 2)。

      如果 正序遍历(从 j = 1j = 2):

      dp[1] = dp[1 - 1] + 15 = 15   // 物品 0 放入背包
      dp[2] = dp[2 - 1] + 15 = 30   // 物品 0 再次放入背包!(重复了)
      

      最终 dp[2] = 30,意味着物品 0 被放了两次,但 0-1 背包问题不允许这么做!

      2. 倒序遍历的正确性

      我们换成 倒序遍历(从 j = 2j = 1):

      dp[2] = dp[2 - 1] + 15 = 15  // 物品 0 放入背包
      dp[1] = dp[1 - 1] + 15 = 15  // 物品 0 放入背包
      

      最终 dp[2] = 15物品 0 只放了一次,这样就符合 0-1 背包的规则。

      总结:

      • 正序遍历时,当前 dp[j] 依赖的是本轮已经更新的 dp[j - weight[i]],导致物品可能被重复放入。(因为正序遍历j是从小到大遍历)【】

      • 倒序遍历时,当前 dp[j] 依赖的是上一轮的 dp[j - weight[i]],不会影响还未计算的 dp[j - weight[i]],保证每件物品只放入一次。【上一轮指的是物品 i 还没被处理时的 dp 状态(也可以理解为 dp 还没更新前的状态)】

      为什么二维 DP 不需要倒序遍历?

      二维 DP dp[i][j] 记录i 个物品容量 j 时的最大价值:

      dp[i][j] = max(dp[i-1][j], dp[i-1][j - weight[i]] + value[i])
      

      这里 dp[i][j]从上一行 dp[i-1] 计算出来的,本层不会影响本层的其他 dp[i][j],自然不会导致物品重复放入。因此,二维 DP 可以正序遍历

      总结:

      • 二维 DP 计算 dp[i][j] 时,使用的是上一行 dp[i-1][j],不会影响当前行,所以可以正序遍历。
      • 一维 DP(滚动数组) 由于 dp[j] 直接覆盖 dp[j - weight[i]],需要倒序遍历,防止物品重复放入。

10.分割等和子集

给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

思路:

为什么这道题可以用01背包写 算出所有数字的sum/2为target

然后 如果 数组里面 有一部分数字 可以填满target包 说明剩下一部分的和也是taget 那么可以分割成两个等和子集

class Solution {
    public boolean canPartition(int[] nums) {
        if(nums.length==0){  return false; }
        int sum=0;
        for(int i=0;i<nums.length;i++){
            sum+=nums[i];
        }
        //总和为奇数 不能平分
        if(sum%2!=0) return false;
        int target=sum/2;

        int dp[]=new int[target+1];//注意这里dp数组的大小是背包容量的大小

        //转换为nums数组能否填满容量为target的背包
        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]);
            }
            //剪枝一下,每一次完成内层循环,立即检查
            if(dp[target] == target)
                return true;
        }
         return dp[target]==target;
    }
}

11.最后一块石头的重量

有一堆石头,每块石头的重量都是正整数。

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

如果 x == y,那么两块石头都会被完全粉碎;

如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。

最后,最多只会剩下一块石头。返回此石头最小的可能重量。如果没有石头剩下,就返回 0。

示例:

  • 输入:[2,7,4,1,8,1]
  • 输出:1

思路

与上一题类似,只不过最后返回的不一样

class Solution {
    public int lastStoneWeightII(int[] stones) {
     int sum = 0;
        for (int i : stones) {
            sum += i;
        }
        int target = sum >> 1;
        //初始化dp数组
        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];

    }
}

12.目标和

给定一个非负整数数组,a1, a2, …, an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。

返回可以使最终数组和为目标数 S 的所有添加符号的方法数。

示例:

  • 输入:nums: [1, 1, 1, 1, 1], S: 3
  • 输出:5

解释:

  • -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

一共有5种方法让最终目标和为3。

  • 思路

本题要如何使表达式结果为target,

既然为target,那么就一定有 left组合 - right组合 = target。

left + right = sum,而sum是固定的。right = sum - left

left - (sum - left) = target 推导出 left = (target + sum)/2 。

target是固定的,sum是固定的,left就可以求出来。

此时问题就是在集合nums中找出和为left的组合。那么剩下那部分就是right了

0/1 背包的本质是:从给定集合中选择部分元素,使其和达到特定目标值

  • 二维数组

此时问题就转化为,用nums装满容量为left的背包,有几种方法

if ((target + sum) % 2 == 1) return 0; // 此时没有方案
if (abs(target) > sum) return 0; // 此时没有方案

因为每个物品(题目中的1)只用一次!

  • 确定dp数组以及下标的含义
    • dp[i][j]:使用 下标为[0, i]的nums[i]能够凑满j(包括j)这么大容量的包,有dp[i][j]种方法。
  • 确定递推公式
    • 不放物品i:即背包容量为j,里面不放物品i,装满有dp[i - 1][j]中方法。
    • 放物品i: 即:先空出物品i的容量,背包容量为(j - 物品i容量),放满背包有 dp[i - 1][j - 物品i容量] 种方法。
      • 因为dp[i - 1][j - 物品i容量]这个前面已经算出来了
  • 一维数组版本
class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int sum=0;
        for(int num:nums){
            sum+=num;
        }
        int x=(target+sum)/2;
        //提前剪枝
        //表达式的范围为[-sum,sum]
        if(Math.abs(target)>sum){
            return 0;
        }

        if((target+sum)%2==1){
            return 0;
        }
        int dp[]= new int[x+1];
        dp[0]=1;
        for(int i=0;i<nums.length;i++){
            for(int j=x;j>=nums[i];j--){
                dp[j]+=dp[j-nums[i]];
            }
        }
        return dp[x];
    }
}

13.一和零

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

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

如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。

示例 1:

  • 输入: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 。

示例 2:

  • 输入:strs = [“10”, “0”, “1”], m = 1, n = 1
  • 输出:2
  • 解释:最大的子集是 {“0”, “1”} ,所以答案是 2 。

思路

本题中strs 数组里的元素就是物品,每个物品都是一个

而m 和 n相当于是一个背包,两个维度的背包

  • dp[i][j]:最多有i个0和j个1的strs的最大子集的大小为dp[i][j]

  • 内部两个for循环都是从后往前遍历

  • 例子

假设 strs = ["10", "01", "1"]m = 2(最多 2 个 0),n = 2(最多 2 个 1)。

我们希望找到最多能选多少个字符串,使得 0 的数量 ≤ m1 的数量 ≤ n

第一步:加入 “10”

“10” 里 zeroNum = 1oneNum = 1

如果从前往后更新

for (int i = 1; i <= m; i++) {  // 从 1 到 m
    for (int j = 1; j <= n; j++) {  // 从 1 到 n
        dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - 1] + 1);
    }
}

按照 从前往后 更新 dp

dp[1][1] = max(dp[1][1], dp[0][0] + 1) = max(0, 1) = 1
dp[1][2] = max(dp[1][2], dp[0][1] + 1) = max(0, 1) = 1
dp[2][1] = max(dp[2][1], dp[1][0] + 1) = max(0, 1) = 1
dp[2][2] = max(dp[2][2], dp[1][1] + 1) = max(0, 2) = 2 ❌【错误的累加】

dp[2][2] 错误地加了 2,因为 dp[1][1] 已经是 1 了,而 dp[1][1] 也在本轮更新时被改变了! (应该在加入第二个字符串的时候,dp[2][2]才会去使用比加入第一个字符串时候的dp[1][1]


按照 从后往前 更新 dp

dp[2][2] = max(dp[2][2], dp[1][1] + 1) = max(0, 1) = 1 ✅【正确】
dp[2][1] = max(dp[2][1], dp[1][0] + 1) = max(0, 1) = 1
dp[1][2] = max(dp[1][2], dp[0][1] + 1) = max(0, 1) = 1
dp[1][1] = max(dp[1][1], dp[0][0] + 1) = max(0, 1) = 1

因为 dp[1][1] 没有被提前更新,所以 dp[2][2] 计算时仍然使用旧值,不会发生

class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        int[][] dp = new int[m + 1][n + 1];  // 创建 DP 数组
        int zeroNum, oneNum;

        // 遍历每个字符串,相当于遍历物品
        for (String s : strs) {
            zeroNum = 0;
            oneNum = 0;
            
            // 计算当前字符串中的 0 和 1 的数量
            for (char c : s.toCharArray()) {
                if (c == '0') {
                    zeroNum++;
                } else {
                    oneNum++;
                }
            }

            // 从后向前遍历,确保每个字符串只能被计算一次(0-1 背包)
            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);//这里+1是因为加入了当前str
                }
            }
        }
        return dp[m][n];
    }
}

14.完全背包理论基础

有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。

完全背包和01背包问题唯一不同的地方就是,每种物品有无限件

dp[i][j] 表示从下标为[0-i]的物品,每个物品可以取无限次,放进容量为j的背包,价值总和最大是多少

在01背包中,背包先空留出物品1的容量,此时容量为1,只考虑放物品0的最大价值是 dp[0][1]因为01背包每个物品只有一个,既然空出物品1,那背包中也不会再有物品1

而在完全背包中,物品是可以放无限个,所以 即使空出物品1空间重量,那背包中也可能还有物品1,所以此时我们依然考虑放 物品0 和 物品1 的最大价值即:dp[1][1], 而不是 dp[0][1]

所以 放物品1 的情况 = dp[1][1] + 物品1 的价值,推导方向如图:

在这里插入图片描述

以上过程,抽象化如下:

  • 不放物品i:背包容量为j,里面不放物品i的最大价值是dp[i - 1][j]
  • 放物品i:背包空出物品i的容量后,背包容量为j - weight[i]dp[i][j - weight[i]] 为背包容量为j - weight[i]且不放物品i的最大价值,那么dp[i][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值

递推公式: dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]);

(注意,完全背包二维dp数组 和 01背包二维dp数组 递推公式的区别,01背包中是 dp[i - 1][j - weight[i]] + value[i])

  • 初始化

    • 首先从dp[i][j]的定义出发,如果背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为0。

    • 状态转移方程 dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]); 可以看出有一个方向 i 是由 i-1 推导出来,那么i为0的时候就一定要初始化。

      dp[0][j],即:存放编号0的物品的时候,各个容量的背包所能存放的最大价值。

      那么很明显当 j < weight[0]的时候,dp[0][j] 应该是 0,因为背包容量比编号0的物品重量还小。

      j >= weight[0]时,dp[0][j] 如果能放下weight[0]的话,就一直装,每一种物品有无限个

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt();
        int bagWeight = scanner.nextInt();

        int[] weight = new int[n];
        int[] value = new int[n];

        for (int i = 0; i < n; i++) {
            weight[i] = scanner.nextInt();
            value[i] = scanner.nextInt();
        }

        int[][] dp = new int[n][bagWeight + 1];

        // 初始化(只要初始化 当放入物品0时,各种背包容量的情况)
        for (int j = weight[0]; j <= bagWeight; j++) {
            dp[0][j] = dp[0][j - weight[0]] + value[0];
        }

        // 动态规划
        for (int i = 1; i < n; 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][j - weight[i]] + value[i]);
                }
            }
        }

        System.out.println(dp[n - 1][bagWeight]);
        scanner.close();
    }
}

15.零钱兑换2

给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。

示例 1:

  • 输入: amount = 5, coins = [1, 2, 5]
  • 输出: 4

解释: 有四种方式可以凑成总金额:

  • 5=5
  • 5=2+2+1
  • 5=2+1+1+1
  • 5=1+1+1+1+1

思路

这道题就是一个完全背包的模板题

class Solution {
    public int change(int amount, int[] coins) {
        int[][] dp = new int[coins.length][amount+1];

        // 初始化边界值
        for(int i = 0; i < coins.length; i++){
            // 第一列的初始值为1
            dp[i][0] = 1;
        }
        for(int j = coins[0]; j <= amount; j++){
            // 初始化第一行
            dp[0][j] += dp[0][j-coins[0]];
        }
        
        for(int i = 1; i < coins.length; i++){
            for(int j = 1; j <= amount; j++){
                if(j < coins[i]) dp[i][j] = dp[i-1][j];
                else dp[i][j] = dp[i][j-coins[i]] + dp[i-1][j];
            }
        }

        return dp[coins.length-1][amount];
    }
}
  • 如果写成一维,要注意遍历顺序

    • 本题中我们是外层for循环遍历物品(钱币),内层for遍历背包(金钱总额),还是外层for遍历背包(金钱总额),内层for循环遍历物品(钱币)呢?

      完全背包的一维dp中讲解了两个for循环的先后顺序都是可以的。

    • 但本题就不行了!

    • 因为纯完全背包求得装满背包的最大价值是多少,和凑成总和的元素有没有顺序没关系,即:有顺序也行,没有顺序也行!

      而本题要求凑成总和的组合数,元素之间明确要求没有顺序。

  • 此题求组合数,先确定硬币,再根据总金额加减

    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];
        }
    }
    
    • 注意:因为完全背包每种物品有无限个,不用倒序遍历,因为不需要防止重复计算,但是01背包和多重背包都要倒序(物品数量为有限个)

第一种方式(组合数):先选用什么硬币,再考虑总价

想象你在菜市场买菜,你手里有面额为 1 元和 5 元的硬币,现在你想用它们刚好凑出 6 元。

步骤

  1. 先考虑 1 元硬币
    • 你可以用 6 枚 1 元硬币来凑出 6 元。
    • 你可以用 5 枚 1 元 + 1 枚 5 元凑出 6 元。
    • 你可以用 4 枚 1 元 + 1 枚 5 元(但这种情况和前面的一样,只是顺序不同)。
    • 只要先选定某个硬币,后面就不会改变它们的顺序,这样避免了重复计算。
  2. 然后考虑 5 元硬币
    • 你可以直接用 1 枚 5 元硬币 + 1 枚 1 元硬币凑出 6 元。

这种方式的特点是,先固定一种硬币,再逐步凑钱,这样不会重复计算同一个组合的不同排列方式。所以它得到的是组合数

第二种方式(排列数):先考虑总价,再随意加钱

如果我们换个方式,每次不管顺序,直接看能不能用 1 元和 5 元硬币凑到 6 元:

  • 可能先选 1 元,再选 5 元(1 → 5)。
  • 也可能先选 5 元,再选 1 元(5 → 1)。
  • 这样一来,{1, 5} 和 {5, 1} 被当作两种不同的方法计算了。
  • 这时候,顺序被考虑了进来,所以计算出来的是排列数

总结

  • 先遍历硬币(外层循环是硬币),再遍历金额(内层循环是金额)组合数
  • 先遍历金额(外层循环是金额),再遍历硬币(内层循环是硬币)排列数

16.组合总和

给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。

示例:

  • nums = [1, 2, 3]
  • target = 4

所有可能的组合为: (1, 1, 1, 1) (1, 1, 2) (1, 2, 1) (1, 3) (2, 1, 1) (2, 2) (3, 1)

请注意,顺序不同的序列被视作不同的组合。

  • 经过上一题分析可以很容易得出

    class Solution {
        public int combinationSum4(int[] nums, int target) {
            int dp[] = new int [target+1];
            dp[0]=1;
            for(int j=0;j<=target;j++){
                 for(int i=0;i<nums.length;i++){
                    if(j>=nums[i]){
                        dp[j]+=dp[j-nums[i]];
                    }
                }       
            }
            return dp[target];
        }
    }
    

17.爬楼梯进阶版

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

每次你可以爬至多m (1 <= m < n)个台阶。你有多少种不同的方法可以爬到楼顶呢?

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

输入描述:输入共一行,包含两个正整数,分别表示n, m

输出描述:输出一个整数,表示爬到楼顶的方法数。

输入示例:3 2

输出示例:3

提示:

当 m = 2,n = 3 时,n = 3 这表示一共有三个台阶,m = 2 代表你每次可以爬一个台阶或者两个台阶。

此时你有三种方法可以爬到楼顶。

  • 1 阶 + 1 阶 + 1 阶段

  • 1 阶 + 2 阶

  • 2 阶 + 1 阶

  • 思路 背包容量为n ,可以看做有1阶,2阶…n阶的物品,每一阶有无数个

  • 完全背包,求排列问题,先遍历背包,再遍历物品

import java.util.Scanner;
class climbStairs{
    public static void main(String [] args){
        Scanner sc = new Scanner(System.in);
        int m, n;

        // 从键盘输入参数,中间用空格隔开
        n = sc.nextInt();
        m = sc.nextInt();

        // 求排列问题,先遍历背包再遍历物品
        int[] dp = new int[n + 1];
        dp[0] = 1;
        for (int j = 1; j <= n; j++) {
            for (int i = 1; i <= m; i++) {
                if (j - i >= 0) dp[j] += dp[j - i];
            }
        }
        System.out.println(dp[n]);
    
    }
}

18.零钱兑换

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

你可以认为每种硬币的数量是无限的。

示例 1:

  • 输入:coins = [1, 2, 5], amount = 11
  • 输出:3
  • 解释:11 = 5 + 5 + 1
class Solution {
    public int coinChange(int[] coins, int amount) {
        int max = Integer.MAX_VALUE;
        int[] dp = new int[amount + 1];
        //初始化dp数组为最大值 表示这些还无法凑出
        for (int j = 0; j < dp.length; j++) {
            dp[j] = max;
        }
        //当金额为0时需要的硬币数目为0
        dp[0] = 0;
        for (int i = 0; i < coins.length; i++) {
            //正序遍历:完全背包每个硬币可以选择多次
            for (int j = coins[i]; j <= amount; j++) {
                //只有dp[j-coins[i]]不是初始最大值时,该位才有选择的必要
                if (dp[j - coins[i]] != max) {
                    //选择硬币数目最小的情况
                    dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1);
                 	// dp[j - coins[i]]:凑成 j - coins[i] 需要的最少硬币数。
					//+1:因为我们选了 coins[i] 这个硬币,所以总硬币数要 +1。
                }
            }
        }
        return dp[amount] == max ? -1 : dp[amount];
    }
}

什么时候是 Math.min,什么时候是 +=,什么时候是 +1

情况公式解释
最小值问题dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1);硬币找零问题,需要选择最少硬币数,所以取最小值。
最大值问题dp[j] = Math.max(dp[j], dp[j - coins[i]] + 1);例如选取最多的元素时,取最大值。
组合数问题(求方案数)dp[j] += dp[j - coins[i]];计算方案总数时,累加方案数。

19.完全平方数

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

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

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

  • 思路 :同上题
class Solution {
    public int numSquares(int n) {
        int max = Integer.MAX_VALUE;
        int[] dp = new int[n + 1];
        for (int j = 0; j <=n ; j++) {
            dp[j] = max;
        }
        dp[0]=0;
        //完全背包,先遍历物品,再遍历背包
        for(int i=1;i*i<=n;i++){
            for(int j=1;j<=n;j++){
                if(j>=i*i&&dp[j-i*i]!=max){
                    dp[j]=Math.min(dp[j],dp[j-i*i]+1);
                }
            }
        }
        return dp[n];
    }
}

20.单词拆分

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

说明:

拆分时可以重复使用字典中的单词。

你可以假设字典中没有重复的单词。

示例 1:

  • 输入: s = “leetcode”, wordDict = [“leet”, “code”]
  • 输出: true
  • 解释: 返回 true 因为 “leetcode” 可以被拆分成 “leet code”。

示例 2:

  • 输入: s = “applepenapple”, wordDict = [“apple”, “pen”]
  • 输出: true
  • 解释: 返回 true 因为 “applepenapple” 可以被拆分成 “apple pen apple”。
  • 注意你可以重复使用字典中的单词。
  1. 确定dp数组以及下标的含义
  • dp[i] : 字符串长度为i的话,dp[i]为true,表示可以拆分为一个或多个在字典中出现的单词。
  1. 确定递推公式
  • 如果确定dp[j] 是true,且 [j, i] 这个区间的子串出现在字典里,那么dp[i]一定是true。(j < i )。

  • 所以递推公式是 if([j, i] 这个区间的子串出现在字典里 && dp[j]是true) 那么 dp[i] = true

  1. dp数组如何初始化
  • 从递推公式中可以看出,dp[i] 的状态依靠 dp[j]是否为true,那么dp[0]就是递推的根基,dp[0]一定要为true,否则递推下去后面都都是false了。

  • 那么dp[0]有没有意义呢?

    • dp[0]表示如果字符串为空的话,说明出现在字典里。

    • 但题目中说了“给定一个非空字符串 s” 所以测试数据中不会出现i为0的情况,那么dp[0]初始为true完全就是为了推导公式。

  • 下标非0的dp[i]初始化为false,只要没有被覆盖说明都是不可拆分为一个或多个在字典中出现的单词。

  1. 确定遍历顺序
  • 题目中说是拆分为一个或多个在字典中出现的单词,所以这是完全背包。

  • 求排列数就是外层for遍历背包,内层for循环遍历物品

import java.util.*;

class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        HashSet<String> dict = new HashSet<>(wordDict); // 用 HashSet 加速查找
        boolean[] dp = new boolean[s.length() + 1]; // dp[i] 表示前 i 个字符是否能被拆分
        dp[0] = true; // 空字符串可以被拆分

        // 外层循环是字符串的长度
        for (int j = 1; j <= s.length(); j++) {
            // 内层循环是从 0 到 j-1,检查是否能拆分
            for (int i = 0; i < j; i++) {
                if (dp[i] && dict.contains(s.substring(i, j))) {
                    dp[j] = true;
                    break; // 只要找到一种方式即可,无需继续遍历
                }
            }
        }
        return dp[s.length()];
    }
}

21.多重背包理论基础

有N种物品和一个容量为V 的背包。第i种物品最多有Mi件可用,每件耗费的空间是Ci ,价值是Wi 。求解将哪些物品装入背包可使这些物品的耗费的空间 总和不超过背包容量,且价值总和最大。

多重背包和01背包是非常像的, 为什么和01背包像呢?

每件物品最多有Mi件可用,把Mi件摊开,其实就是一个01背包问题了。

import java.util.Scanner;
class multi_pack{
    public static void main(String [] args) {
        Scanner sc = new Scanner(System.in);

        /**
         * bagWeight:背包容量
         * n:物品种类
         */
        int bagWeight, n;
        
        //获取用户输入数据,中间用空格隔开,回车键换行
        bagWeight = sc.nextInt();
        n = sc.nextInt();
        int[] weight = new int[n];
        int[] value = new int[n];
        int[] nums = new int[n];
        for (int i = 0; i < n; i++) weight[i] = sc.nextInt();
        for (int i = 0; i < n; i++) value[i] = sc.nextInt();
        for (int i = 0; i < n; i++) nums[i] = sc.nextInt();

        int[] dp = new int[bagWeight + 1];

        //先遍历物品再遍历背包,作为01背包处理
        for (int i = 0; i < n; i++) {
            for (int j = bagWeight; j >= weight[i]; j--) {//一维倒序
                //遍历每种物品的个数
                for (int k = 1; k <= nums[i] && (j - k * weight[i]) >= 0; k++) {
                    dp[j] = Math.max(dp[j], dp[j - k * weight[i]] + k * value[i]);
                }
            }
        }
        System.out.println(dp[bagWeight]);
    }
}

22.打家劫舍

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

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

  • 示例 1:
  • 输入:[1,2,3,1]
  • 输出:4

解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。 偷窃到的最高金额 = 1 + 3 = 4 。

思路

  • dp[i]:下标i以内的房屋,最多可以偷窃的金额为dp[i]
  • 递推公式:
    • 如果偷第i房间,那么dp[i] = dp[i - 2] + nums[i] ,即:第i-1房一定是不考虑的,找出 下标i-2(包括i-2)以内的房屋,最多可以偷窃的金额为dp[i-2] 加上第i房间偷到的钱。
    • 如果不偷第i房间,那么dp[i] = dp[i - 1],即考 虑i-1房,(注意这里是考虑,并不是一定要偷i-1房,这是很多同学容易混淆的点
    • 然后dp[i]取最大值,即dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
  • 初始化
    • 从dp[i]的定义上来讲,dp[0] 一定是 nums[0],dp[1]就是nums[0]和nums[1]的最大值即:dp[1] = max(nums[0], nums[1]);
class Solution {
    public int rob(int[] nums) {
        if (nums == null || nums.length == 0) return 0;
		if (nums.length == 1) return nums[0];
        int n= nums.length;
        int[] dp = new int[n];
        dp[0]=nums[0];
        dp[1]=Math.max(nums[0],nums[1]);
        for(int i=2;i<n;i++){
            dp[i]=Math.max(dp[i-1],dp[i-2]+nums[i]);
        }
        return dp[n-1];
    }
}

23.打家劫舍2

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

给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,能够偷窃到的最高金额。

示例 1:

  • 输入:nums = [2,3,2]
  • 输出:3
  • 解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。

思路

对于一个数组,成环的话主要有如下三种情况:

  • 情况一:考虑不包含首尾元素

  • 情况二:考虑包含首元素,不包含尾元素

  • 情况三:考虑包含尾元素,不包含首元素

注意我这里用的是"考虑",例如情况三,虽然是考虑包含尾元素,但不一定要选尾部元素! 对于情况三,取nums[1] 和 nums[3]就是最大的。

而情况二 和 情况三 都包含了情况一了,所以只考虑情况二和情况三就可以了

class Solution {
    public int rob(int[] nums) {
        if (nums.length == 0) return 0;
        if (nums.length == 1) return nums[0];

        // 计算两种情况:
        // 1. 偷第一间房子,不偷最后一间
        // 2. 偷最后一间房子,不偷第一间
        int res1 = robRange(nums, 0, nums.length - 2);
        int res2 = robRange(nums, 1, nums.length - 1);
        return Math.max(res1, res2);
    }

    private int robRange(int[] nums, int start, int end) {
        if (start > end) return 0; // 边界情况
        if (start == end) return nums[start]; // 只有一个房子

        int n = end - start + 1;
        int[] dp = new int[n]; // 创建DP数组

        // 初始化DP数组
        dp[0] = nums[start];
        dp[1] = Math.max(nums[start], nums[start + 1]);

        // 状态转移
        for (int i = 2; i < n; i++) {
            dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[start + i]);
        }

        return dp[n - 1]; // 返回最终能偷到的最大金额
    }
}

24.打家劫舍3

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

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

思路

  • 本题一定是要后序遍历,因为通过递归函数的返回值来做下一步计算

  • 如果抢了当前节点,两个孩子就不能动,如果没抢当前节点,就可以考虑抢左右孩子(注意这里说的是“考虑”

  • int dp[] = new int[2]; 新建一个长度为2的数组

    • 下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱
  • 那么有同学可能疑惑,长度为2的数组怎么标记树中每个节点的状态呢?

    别忘了在递归的过程中,系统栈会保存每一层递归的参数

  • 遍历顺序:

    • 首先明确的是使用后序遍历。 因为要通过递归函数的返回值来做下一步计算
    • 通过递归左节点,得到左节点偷与不偷的金钱。
    • 通过递归右节点,得到右节点偷与不偷的金钱。
  • 单层递归的逻辑

    • 不偷:Max(左孩子不偷,左孩子偷) + Max(右孩子不偷,右孩子偷)
    • 偷:左孩子不偷+ 右孩子不偷 + 当前节点偷
class Solution {
    public int rob(TreeNode root) {
        int[] res = robAction(root);
        return Math.max(res[0], res[1]);
    }

    int[] robAction(TreeNode root) {
        if (root == null) {
            return new int[]{0, 0}; // 叶子节点返回[0,0]
        }

        int[] left = robAction(root.left);
        int[] right = robAction(root.right);

        int[] res = new int[2];
        res[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]); // 不偷当前节点
        res[1] = root.val + left[0] + right[0]; // 偷当前节点

        return res;
    }
}

25.买卖股票的最佳时机

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

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

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。

  • 示例 1:
  • 输入:[7,1,5,3,6,4]
  • 输出:5
    解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。

思路

  • dp[i][0] 表示第i天持有股票所得最多现金,dp[i][1] 表示第i天不持有股票所得最多现金

  • 递推公式

    • 如果第i天持有股票即dp[i][0], 那么可以由两个状态推出来

      • 那么dp[i][0]应该选所得现金最大的,所以dp[i][0] = max(dp[i - 1][0], -prices[i]);
      • (这里是-prices[i]是因为只能买入一次,这一次买入前都没有现金)
    • 如果第i天不持有股票即dp[i][1], 也可以由两个状态推出来

      同样dp[i][1]取最大的,dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]);

  • 初始化

    • 那么dp[0][0]表示第0天持有股票,此时的持有股票就一定是买入股票了,因为不可能有前一天推出来,所以dp[0][0] -= prices[0];

      dp[0][1]表示第0天不持有股票,不持有股票那么现金就是0,所以dp[0][1] = 0

class Solution {
    public int maxProfit(int[] prices) {
        if(prices==null||prices.length==0){
            return 0;
        }
        int n = prices.length;
        int[][] dp = new int[n][2];
        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],-prices[i]);
            //不持有的话,可能前一天也没有持有 或者 今天刚卖出去
            dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]+prices[i]);
        }
        return Math.max(dp[n-1][0],dp[n-1][1]);
    }
}

26.买卖股票的最佳时机2

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

class Solution {
    public int maxProfit(int[] prices) {
        if (prices == null || prices.length == 0) {
            return 0;
        }
        int n = prices.length;
        int[][] dp = new int[n][2];

        // 初始化第一天的状态
        dp[0][0] = -prices[0]; // 第一天买入股票
        dp[0][1] = 0;          // 第一天不持有股票

        for (int i = 1; i < n; i++) {
            // 持有股票的最大利润(保持之前的持有状态 or 今天买入)
            dp[i][0] = Math.max(dp[i - 1][0], dp[i-1][1]-prices[i]);

            // 不持有股票的最大利润(保持之前的不持有状态 or 今天卖出)
            dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
        }

        // 返回最后一天不持有股票的利润
        return dp[n - 1][1];
    }
}

27.买卖股票的最佳时机3

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

思路

  • 确定dp数组以及下标的含义

一天一共就有五个状态,

  1. 没有操作 (其实我们也可以不设置这个状态)
  2. 第一次持有股票
  3. 第一次不持有股票
  4. 第二次持有股票
  5. 第二次不持有股票

dp[i][j]中 i表示第i天,j为 [0 - 4] 五个状态,dp[i][j]表示第i天状态j所剩最大现金

  • 初始化

第0天没有操作,这个最容易想到,就是0,即:dp[0][0] = 0;

第0天做第一次买入的操作,dp[0][1] = -prices[0];

第0天做第一次卖出的操作,这个初始值应该是多少呢?

此时还没有买入,怎么就卖出呢? 其实大家可以理解当天买入,当天卖出,所以dp[0][2] = 0;

第0天第二次买入操作,初始值应该是多少呢?应该不少同学疑惑,第一次还没买入呢,怎么初始化第二次买入呢?

第二次买入依赖于第一次卖出的状态,其实相当于第0天第一次买入了,第一次卖出了,然后再买入一次(第二次买入),那么现在手头上没有现金,只要买入,现金就做相应的减少。

所以第二次买入操作,初始化为:dp[0][3] = -prices[0];

同理第二次卖出初始化dp[0][4] = 0;

class Solution {
    public int maxProfit(int[] prices) {
        int len = prices.length;
        if(len==0) return 0;
        int[][] dp = new int[len][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<len;i++){
            dp[i][1]=Math.max(dp[i-1][1],-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[len - 1][4];
    }
}

##28.买卖股票的最佳时机4

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

设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

思路

  • 初始化
    • 除了0以外,偶数就是卖出,奇数就是买入,题目要求是至多有K笔交易,那么j的范围就定义为 2 * k + 1 就可以了。
    • 参考上题
class Solution {
    public int maxProfit(int k, int[] prices) {
        int len = prices.length;
        if(len<=1) return 0;
        int[][] dp = new int[len][2*len+1];
        for(int i=1;i<2*len;i+=2){
            dp[0][i]=-prices[0];
        }
        //k支彩票 那么下标就要到2*k
        for(int i=1;i<len;i++){
            for(int j=0;j<2*k-1;j+=2){ //因为考虑到+2可能会越界
                //单数是买进 双数是卖出
                dp[i][j+1]=Math.max(dp[i-1][j+1],dp[i-1][j]-prices[i]);
                dp[i][j+2]=Math.max(dp[i-1][j+2],dp[i-1][j+1]+prices[i]);
            }
        }
        return dp[len-1][k*2];
    }
}

29.买卖股票最佳时期含冷冻期

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

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

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

思路

  • 状态一:持有股票状态(今天买入股票,或者是之前就买入了股票然后没有操作,一直持有)

  • 不持有股票状态,这里就有两种卖出股票状态

    • 状态二:保持卖出股票的状态(两天前就卖出了股票,度过一天冷冻期。或者是前一天就是卖出股票状态,一直没操作)
    • 状态三:今天卖出股票
  • 状态四:今天为冷冻期状态,但冷冻期状态不可持续,只有一天!

  • 思考:

    • 「今天卖出股票」我是没有单独列出一个状态的归类为「不持有股票的状态」,而本题为什么要单独列出「今天卖出股票」 一个状态呢?
    • 因为本题我们有冷冻期,而冷冻期的前一天,只能是 「今天卖出股票」状态,如果是 「不持有股票状态」那么就很模糊,因为不一定是 卖出股票的操作。
  • 递推公式:

    • 达到买入股票状态(状态一)即:dp[i][0],有两个具体操作:

      • 操作一:前一天就是持有股票状态(状态一),dp[i][0] = dp[i - 1][0]
      • 操作二:今天买入了,有两种情况
        • 前一天是冷冻期(状态四),dp[i - 1][3] - prices[i]
        • 前一天是保持卖出股票的状态(状态二),dp[i - 1][1] - prices[i]

      那么dp[i][0] = max(dp[i - 1][0], dp[i - 1][3] - prices[i], dp[i - 1][1] - prices[i]);

      达到保持卖出股票状态(状态二)即:dp[i][1],有两个具体操作:

      • 操作一:前一天就是状态二
      • 操作二:前一天是冷冻期(状态四)

      dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]);

      达到今天就卖出股票状态(状态三),即:dp[i][2] ,只有一个操作:

      昨天一定是持有股票状态(状态一),今天卖出

      即:dp[i][2] = dp[i - 1][0] + prices[i];

      达到冷冻期状态(状态四),即:dp[i][3],只有一个操作:

      昨天卖出了股票(状态三)

      dp[i][3] = dp[i - 1][2];

30.买卖股票最佳时期含手续费

31.最长上升子序列

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

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

示例 1:

  • 输入:nums = [10,9,2,5,3,7,101,18]
  • 输出:4
  • 解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

思路

  • dp[i]表示i之前包括i的以nums[i]结尾的最长递增子序列的长度
class Solution {
    public int lengthOfLIS(int[] nums) {
        int len = nums.length;
        if(len<=1){
            return len;
        }
        int dp[] = new int[len];
        Arrays.fill(dp,1);//初始化
        int res=1; //存储以每个数字作为结尾的最大值
        for(int i=1;i<len;i++){
            for(int j=0;j<i;j++){
                if(nums[i]>nums[j]){
                    dp[i]=Math.max(dp[i],dp[j]+1);
                }
            }
            res=Math.max(dp[i],res);
        }
        return res;
    }
}

32.最长连续递增序列

class Solution {
    public int findLengthOfLCIS(int[] nums) {
        int len = nums.length;
        if (len <= 1) return len;
        int[] dp = new int[len]; 
        Arrays.fill(dp,1);
        int res = 0;
        for(int i=1;i<len;i++){
            if(nums[i]>nums[i-1]){
                dp[i]=Math.max(dp[i],dp[i-1]+1);
            }
            res=Math.max(res,dp[i]);
        }
        return res;
    }

}

33.最长重复子数组

给两个整数数组 nums1nums2 ,返回 两个数组中 公共的 、长度最长的子数组的长度

思路

  • dp[i][j] :以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组长度为dp[i][j]。 (特别注意: “以下标i - 1为结尾的A” 标明一定是 以A[i-1]为结尾的字符串 )
  • 如果用dp[i][j]表示以下标i 为结尾的A,和以下标j 为结尾的B,最长重复子数组长度为dp[i][j],那么dp数组要对第一行和第一列进行初始化,而且在循环内部判断的时候,由于dp[i][j] = dp[i - 1][j - 1] + 1;,还要判断i>0&&j>0,防止数组越界
class Solution {
    public int findLength(int[] nums1, int[] nums2) {
        int result = 0;
        int[][] dp = new int[nums1.length + 1][nums2.length + 1];
        
        for (int i = 1; i < nums1.length + 1; i++) {
            for (int j = 1; j < nums2.length + 1; 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;
    }
}

34.最长公共子序列

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

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

例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。

若这两个字符串没有公共子序列,则返回 0。

思路

  • ``dp[i][j]:长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j]`

  • 递推公式

    • 如果text1[i - 1] 与 text2[j - 1]相同,那么找到了一个公共元素,所以dp[i][j] = dp[i - 1][j - 1] + 1;

    • 如果text1[i - 1] 与 text2[j - 1]不相同,那就看看text1[0, i - 2]与text2[0, j - 1]的最长公共子序列 和 text1[0, i - 1]与text2[0, j - 2]的最长公共子序列,取最大的。

      即:dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);

class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        int len1 = text1.length();
        int len2 = text2.length();
        char[] t1 = text1.toCharArray();
        char[] t2 = text2.toCharArray();
        int[][] dp = new int[len1 + 1][len2 + 1]; // 定义 DP 数组

        // 正确的遍历范围:从 1 到 len1 和 len2
        for (int i = 1; i <= len1; i++) {
            for (int j = 1; j <= len2; j++) {
                if (t1[i - 1] == t2[j - 1]) { // 这里索引需要 -1
                    dp[i][j] = dp[i - 1][j - 1] + 1; // 如果字符相等,LCS 长度+1
                } else {
                    dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]); // 取较大值
                }
            }
        }
        return dp[len1][len2]; 
    }
}

35.不相交的线

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

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

  • nums1[i] == nums2[j]
  • 且绘制的直线不与任何其他连线(非水平线)相交。

请注意,连线即使在端点也不能相交:每个数字只能属于一条连线。

以这种方法绘制线条,并返回可以绘制的最大连线数。

思路

  • 直线不能相交,这就是说明在字符串nums1中 找到一个与字符串nums2相同的子序列,且这个子序列不能改变相对顺序,只要相对顺序不改变,连接相同数字的直线就不会相交。

  • 其实也就是说nums1和nums2的最长公共子序列是[1,4],长度为2。 这个公共子序列指的是相对顺序不变(即数字4在字符串nums1中数字1的后面,那么数字4也应该在字符串nums2数字1的后面)

  • 这么分析完之后,大家可以发现:本题说是求绘制的最大连线数,其实就是求两个字符串的最长公共子序列的长度!

     class Solution {
        public int maxUncrossedLines(int[] nums1, int[] nums2) {
            int len1 = nums1.length;
            int len2 = nums2.length;
            int[][] dp = new int[len1 + 1][len2 + 1];
    
            for (int i = 1; i <= len1; i++) {
                for (int j = 1; j <= len2; 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[len1][len2];
        }
    }
    

36.最大子序和

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

示例:

  • 输入: [-2,1,-3,4,-1,2,1,-5,4]
  • 输出: 6
  • 解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。

思路

  • dp[i]:包括下标i(以nums[i]为结尾)的最大连续子序列和为dp[i]
  • 递推公式:
    • dp[i - 1] + nums[i],即:nums[i]加入当前连续子序列和
    • nums[i],即:从头开始计算当前连续子序列和
    public static int maxSubArray(int[] nums) {
        if (nums.length == 0) {
            return 0;
        }

        int res = nums[0];
        int[] dp = new int[nums.length];
        dp[0] = nums[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;
    }

37.判断子序列

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

字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。

思路

  • 数组含义:dp[i][j] 表示以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度为dp[i][j]

  • 递推公式:

    • if (s[i - 1] == t[j - 1])
      • t中找到了一个字符在s中也出现了
    • if (s[i - 1] != t[j - 1])
      • 相当于t要删除元素,继续匹配

if (s[i - 1] == t[j - 1]),那么dp[i][j] = dp[i - 1][j - 1] + 1;,因为找到了一个相同的字符,相同子序列长度自然要在dp[i-1][j-1]的基础上加1

if (s[i - 1] != t[j - 1]),此时相当于t要删除元素,t如果把当前元素t[j - 1]删除,那么dp[i][j] 的数值就是 看s[i - 1]与 t[j - 2]的比较结果了,即:dp[i][j] = dp[i][j - 1];

class Solution {
    public boolean isSubsequence(String s, String t) {
        int len1 = s.length(); int len2 = t.length();
        int[][] dp = new int[len1+1][len2+1];
        char[] sChar = s.toCharArray();
        char[] tChar = t.toCharArray();
        for(int i=1;i<=len1;i++){
            for(int j=1;j<=len2;j++){
				if(sChar[i]==tChar[j]){
                    dp[i][j]=dp[i-1][j-1]+1;
                }else{
                    dp[i][j]=dp[i][j-1];
                }
            }
        }
        return dp[len1][len2]==len;
    }
}

38.不同的子序列

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

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

题目数据保证答案符合 32 位带符号整数范围

思路

  • dp[i][j]:以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]

  • 递推公式

    • 当s[i - 1] 与 t[j - 1]相等时,dp[i][j]可以有两部分组成。

      • 一部分是用s[i - 1]来匹配,那么个数为dp[i - 1][j - 1]。即不需要考虑当前s子串和t子串的最后一位字母,所以只需要 dp[i-1][j-1](如果匹配了这个字母,那么较短的字符串t就再往前一位)
      • 一部分是不用s[i - 1]来匹配,个数为dp[i - 1][j]
      • 例如: s:bagg 和 t:bag ,s[3] 和 t[2]是相同的,但是字符串s也可以不用s[3]来匹配,即用s[0]s[1]s[2]组成的bag。
      • dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
    • 当s[i - 1] 与 t[j - 1]不相等时,dp[i][j]只有一部分组成,不用s[i - 1]来匹配(就是模拟在s中删除这个元素),即:dp[i - 1][j]

      所以递推公式为:dp[i][j] = dp[i - 1][j];

class Solution {
    public int numDistinct(String s, String t) {
        int len1 = s.length(), len2 = t.length();
        int[][] dp = new int[len1 + 1][len2 + 1];
        final int MOD = 1000000007;

        char[] sChar = s.toCharArray();
        char[] tChar = t.toCharArray();
        //初始化 dp[0][j]:s串为空,没有意义  
        //dp[i][0]=1 有意义 如果t是空串 那么s包含的t的数量就是1
        for (int i = 0; i <= len1; i++) {
            dp[i][0] = 1;
        }

        for (int i = 1; i <= len1; i++) {
            for (int j = 1; j <= len2; j++) {
                if (sChar[i - 1] == tChar[j - 1]) {
                    dp[i][j] = (dp[i - 1][j] + dp[i - 1][j - 1]) % MOD;
                } else {
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }
        return dp[len1][len2];
    }
}

39.两个字符串的删除操作

给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符。

示例:

  • 输入: “sea”, “eat”
  • 输出: 2
  • 解释: 第一步将"sea"变为"ea",第二步将"eat"变为"ea"
class Solution {
    public int minDistance(String word1, String word2) {
        int len1 = word1.length(), len2 = word2.length();
        int[][] dp = new int[len1 + 1][len2 + 1];
        // dp[i][j]指的是word1的[0,...i-1]子串
        //和word2的[0,...j-1]的子串
        // 初始化 dp 数组
        for (int i = 0; i <= len1; i++) dp[i][0] = i; // word2 为空,删除 word1 的所有字符
        for (int j = 0; j <= len2; j++) dp[0][j] = j; // word1 为空,删除 word2 的所有字符

        for (int i = 1; i <= len1; i++) {
            for (int j = 1; j <= len2; 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], dp[i][j - 1]) + 1; // 删除操作 要+1!
                }
            }
        }
        return dp[len1][len2];
    }
}

40.编辑距离

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

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

  • 插入一个字符

    删除一个字符

    替换一个字符

    示例 1:

    输入:word1 = “horse”, word2 = “ros”

    输出:3

    解释: horse -> rorse (将 ‘h’ 替换为 ‘r’) rorse -> rose (删除 ‘r’) rose -> ros (删除 ‘e’)

  • 在整个动规的过程中,最为关键就是正确理解dp[i][j]的定义!

    if (word1[i - 1] != word2[j - 1]),此时就需要编辑了,如何编辑呢?

    • 操作一:word1删除一个元素,那么就是以下标i - 2为结尾的word1 与 j-1为结尾的word2的最近编辑距离 再加上一个操作。即 dp[i][j] = dp[i - 1][j] + 1;
    • 操作二:word2删除一个元素,那么就是以下标i - 1为结尾的word1 与 j-2为结尾的word2的最近编辑距离 再加上一个操作。即 dp[i][j] = dp[i][j - 1] + 1;

    这里有同学发现了,怎么都是删除元素,添加元素去哪了。

    • 操作三:替换元素,word1替换word1[i - 1],使其与word2[j - 1]相同,此时不用增删加元素。 dp[i][j] = dp[i - 1][j - 1] + 1;
class Solution {
    public int minDistance(String word1, String word2) {
        int len1=word1.length();
        int len2=word2.length();
        // 1.初始化dp数组
        // dp[i][j]指的是word1的[0,...i-1]子串
        //和word2的[0,...j-1]的子串
        int[][] dp = new int[len1+1][len2+1];
        // 2.初始化 由于我们把word1添加 等价于 word2删除 所以当word1的下标为0的时候也要初始化
        for (int i = 0; i <= len1; i++) dp[i][0] = i;
        for (int j = 0; j <= len2; j++) dp[0][j] = j;
        for(int i=1;i<=len1;i++){
            for(int j=1;j<=len2;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],Math.min(dp[i][j-1],dp[i-1][j-1]))+1;
                }
                //dp[i-1][j] 代表在word1[1]增加一个字符
                //dp[i][j-1] 表示
            }
        }
        return dp[len1][len2];
    }
}

41.回文子串

给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。

具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

示例 1:

  • 输入:“abc”
  • 输出:3
  • 解释:三个回文子串: “a”, “b”, “c”

示例 2:

  • 输入:“aaa”
  • 输出:6
  • 解释:6个回文子串: “a”, “a”, “a”, “aa”, “aa”, “aaa”

思路

  • 递推公式

    • 一个子字符串(字符串下标范围[i,j])是否回文,依赖于,子字符串(下标范围[i + 1, j - 1])) 是否是回文。

    • 当s[i]与s[j]不相等,那没啥好说的了,dp[i][j]一定是false。

      当s[i]与s[j]相等时,有如下三种情况

      • 情况一:下标i 与 j相同,同一个字符例如a,当然是回文子串
      • 情况二:下标i 与 j相差为1,例如aa,也是回文子串
      • 情况三:下标:i 与 j相差大于1的时候,例如cabac,此时s[i]与s[j]已经相同了,我们看i到j区间是不是回文子串就看aba是不是回文就可以了,那么aba的区间就是 i+1 与 j-1区间,这个区间是不是回文就看dp[i + 1][j - 1]是否为true。
  • 遍历顺序

    • 情况三是根据dp[i + 1][j - 1]是否为true,在对dp[i][j]进行赋值true的。

      dp[i + 1][j - 1]dp[i][j]的左下角

    • 所以一定要从下到上,从左到右遍历,这样保证dp[i + 1][j - 1]都是经过计算的

class Solution {
    public int countSubstrings(String s) {
        int len = s.length();
        char[] chars = s.toCharArray();
        boolean[][] dp = new boolean[len][len];
        int res = 0;

        // 从下往上,从左到右遍历
        for (int i = len - 1; i >= 0; i--) {  // 反向遍历 i,保证 dp[i+1][j-1] 已计算
            for (int j = i; j < len; j++) {   // 这里 j 需要从 i 开始,保证子串 [i,j] 计算正确
                if (chars[i] == chars[j]) {
                    if (j - i <= 1) {   // 单个字符 或 相邻字符相等
                        dp[i][j] = true;
                        res++;
                    } else if(dp[i+1][j-1]){
                        dp[i][j] = dp[i + 1][j - 1];  // 依赖子问题
                        res++;
                    }
                }
                
            }
        }
        return res;
    }
}

42最长回文子序列

给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。

具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

示例 1:

  • 输入:“abc”
  • 输出:3
  • 解释:三个回文子串: “a”, “b”, “c”
class Solution {
    public int longestPalindromeSubseq(String s) {
        char[] chars = s.toCharArray();
        int len =s.length();
        int dp[][] = new int[len][len];
        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(chars[i]==chars[j]){
                    dp[i][j]=dp[i+1][j-1]+2;
                }else{
                    dp[i][j]=Math.max(dp[i+1][j],dp[i][j-1]);
                }
            }
        }
        return dp[0][len-1];
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值