代码随想录_动态规划

news2025/3/22 19:21:37

代码随想录

动态规划

509.斐波那契数

509. 斐波那契数

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

F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1

给定 n ,请计算 F(n)

思路: 动态规划.

1.确定dp数组以及下标的含义: dp[i]的定义为:第i个数的斐波那契数值是dp[i]

2.确定递推公式: dp[i] = dp[i - 1] + dp[i - 2];

3.dp数组如何初始化

dp[0] = 0;
dp[1] = 1;

4.确定遍历顺序: 从递归公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,dp[i]是依赖 dp[i - 1] 和 dp[i - 2],那么遍历的顺序一定是从前到后遍历的

5.举例推导dp数组: 按照这个递推公式dp[i] = dp[i - 1] + dp[i - 2],我们来推导一下,当N为10的时候,dp数组应该是如下的数列:0 1 1 2 3 5 8 13 21 34 55

代码:

class Solution {
    public int fib(int n) {
        // 1. 特殊判断
        if(n <= 1) return n;
        // 2. 定义dp数组
        int[] dp = new int[n+1];
        // 3. 初始化
        dp[0] = 0;
        dp[1] = 1;
        // 4. 依照公式遍历
        for(int i = 2;i <= n;i++) {
            dp[i] = dp[i-1] + dp[i-2];
        }
        // 5. 返回
        return dp[n];
    }
}

简化:

class Solution {
    public int fib(int n) {
        // 1. 特殊判断
        if(n <= 1) return n;
        // 2. 变量代替数组
        int a = 0,b = 1,sum = 0;
        // 3. 公式遍历
        for(int i = 2;i <= n;i++) {
            sum = a + b;
            a = b;
            b = sum;
        }
        // 4. 返回
        return b;
    }
}

70.爬楼梯

70. 爬楼梯

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

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

思路: 动态规划

爬到第n阶: 先爬到第n-1阶,再爬1阶 / 先爬到第n-2阶,再爬两阶 以此递归

递归出口: 爬到第1阶: 1种方法. 爬到第2阶: 2种方法.

代码: 递推公式和斐波那契数相同, 也可以使用变量进行简化

class Solution {
    public int climbStairs(int n) {
        // 下面直接对dp[2]操作, n<=1没有dp[2],防止数组下标越界
        if(n <= 1) return n;
        // 1. 初始化dp数组
        int[] dp = new int[n+1];
        dp[1] = 1;
        dp[2] = 2;
        // 2. 根据递推方式填充数组(i:第i阶台阶. dp[i]:爬到第i阶方式的种类)
        for(int i = 3;i <= n;i++) {
            dp[i] = dp[i-1] + dp[i-2];
        }
        // 3. 返回
        return dp[n];
    }
}

746.使用最小花费爬楼梯

746. 使用最小花费爬楼梯

给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。

你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。

请你计算并返回达到楼梯顶部的最低花费。

思路: 动态规划

  1. 确定dp数组及下标含义: dp[i]: 到达第i台阶所花费的最少体力为dp[i]
  2. 确定递推公式: 有两种途径得到dp[i], dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
  3. dp数组如何初始化: 向前递归+题意: “你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。” 也就是说 到达 第 0 个台阶是不花费的,但从 第0 个台阶 往上跳的话,需要花费 cost[0]。所以初始化 dp[0] = 0,dp[1] = 0;
  4. 确定遍历顺序: 因为是模拟台阶,而且dp[i]由dp[i-1]dp[i-2]推出,所以是从前到后遍历cost数组就可以了。
  5. 举例推导dp数组

代码:

class Solution {
    public int minCostClimbingStairs(int[] cost) {
        // 1. 创建dp数组
        int len = cost.length;
        int[] dp = new int[len+1];
        // 2. 初始化
        dp[0] = 0;
        dp[1] = 0;
        // 3. 根据公式递推
        for(int i = 2;i <= len;i++) {
            dp[i] = Math.min(dp[i-1] + cost[i-1],dp[i-2] + cost[i-2]);
        }
        // 4. 返回
        return dp[len];
    }
}

62.不同路径

62. 不同路径

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

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

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

示例 1:

输入:m = 3, n = 7
输出:28

思路: 动态规划.

  1. 确定dp数组(dp table)以及下标的含义

     dp[i] [j] :表示从(0 ,0)出发,到(i, j) 有dp[i] [j]条不同的路径。
    
  2. 确定递推公式

     想要求dp[i] [j],只能有两个方向来推导出来,即dp[i - 1] [j] 和 dp[i] [j - 1]。
    
     此时在回顾一下 dp[i - 1] [j] 表示啥,是从(0, 0)的位置到(i - 1, j)有几条路径,dp[i][j - 1]同理。
    
     那么很自然,dp[i] [j] = dp[i - 1] [j] + dp[i] [j - 1],因为dp[i] [j]只有这两个方向过来。
    
  3. dp数组的初始化

     如何初始化呢,首先dp[i] [0]一定都是1,因为从(0, 0)的位置到(i, 0)的路径只有一条,那么dp[0][j]也同理。
    
     所以初始化代码为:
    
for (int i = 0; i < m; i++) dp[i][0] = 1;
for (int j = 0; j < n; j++) dp[0][j] = 1;
  1. 确定遍历顺序

     这里要看一下递推公式dp[i] [j] = dp[i - 1] [j] + dp[i] [j - 1],dp[i] [j]都是从其上方和左方推导而来,那么从左到右一层一层遍历就可以了。这样就可以保证推导dp[i] [j]的时候,dp[i - 1] [j] 和 dp[i] [j - 1]一定是有数值的。
    
  2. 举例推导dp数组

代码:

class Solution {
    public int uniquePaths(int m, int n) {
        // 1. 建立dp数组
        int[][] dp = new int[m][n];
        // 2. 初始化
        for(int i = 0;i < m;i++) dp[i][0] = 1;
        for(int j = 0;j < n;j++) dp[0][j] = 1;
        // 3. 遍历填充
        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];
            }
        }
        // 4. 返回
        return dp[m-1][n-1];
    }
}

63.不同路径 II

63. 不同路径 II

给定一个 m x n 的整数数组 grid。一个机器人初始位于 左上角(即 grid[0][0])。机器人尝试移动到 右下角(即 grid[m - 1][n - 1])。机器人每次只能向下或者向右移动一步。

网格中的障碍物和空位置分别用 10 来表示。机器人的移动路径中不能包含 任何 有障碍物的方格。

返回机器人能够到达右下角的不同路径数量。

测试用例保证答案小于等于 2 * 109

示例 1:

输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2
解释:3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右

思路: 动态规划, 具体同上一道. 注意要根据obstacleGrid[][]是否有障碍物来初始化dp, 障碍物所在地方及以下, 以右, 都无法再到达, 因此不需要再手动初始化, 默认为0即可.

代码:

class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        // 1. 创建dp数组
        int m = obstacleGrid.length;
        int n = obstacleGrid[0].length;
        int[][] dp = new int[m][n];
        // 2. 根据obstacleGrid的障碍物分布进行初始化
        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;
        // 3. 按照公式填充dp
        for(int i = 1;i < m;i++) {
            for(int j = 1;j < n;j++) {
                dp[i][j] = obstacleGrid[i][j] == 0 ? dp[i-1][j]+dp[i][j-1] : 0;
            }
        }
        // 4. 返回
        return dp[m-1][n-1];
    }
}

343.整数拆分

343. 整数拆分

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

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

思路: 动态规划.

  1. 确定dp数组(dp table)以及下标的含义
    • dp[i]:分拆数字i,可以得到的最大乘积为dp[i]。
    • dp[i]的定义将贯彻整个解题过程,下面哪一步想不懂了,就想想dp[i]究竟表示的是啥!
  2. 确定递推公式
    • dp[i]最大乘积其实可以从1遍历j,然后有两种渠道得到dp[i].
      • j * (i - j) 直接相乘。
      • j * dp[i - j],相当于是拆分(i - j)
  3. dp的初始化
    • 严格从dp[i]的定义来说,dp[0] dp[1] 就不应该初始化,也就是没有意义的数值。
    • dp[2] = 1,从dp[i]的定义来说,拆分数字2,得到的最大乘积是1
  4. 确定遍历顺序
    • 先遍历物品更好理解
  5. 举例推到dp数组

代码:

class Solution {
    public int integerBreak(int n) {
        // 1. 创建dp数组, i表示要拆分的数值, dp[i]表示拆分后正数乘积的最大值
        int[] dp = new int[n+1];
        // 2. 根据题意从2开始初始化
        dp[2] = 1;
        // 3. 从3开始拆分
        for(int i = 3;i <= n;i++) {// 外层控制当前要拆分的数值
            for(int j = 1;j <= i/2;j++) {// 内层进行拆分, 根据题意从j=1开始
                dp[i] = Math.max( dp[i] , Math.max(j*(i-j),j*dp[i-j]) );
                //                找最大值          拆分一次 继续拆分
            }
        }
        // 4. 返回结果
        return dp[n];
    }
}

96.不同的二叉搜索树

96. 不同的二叉搜索树

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

思路: 动态规划

  1. 确定dp数组(dp table)以及下标的含义

    • dp[i] : 1到i为节点组成的二叉搜索树的个数为dp[i]
    • 也可以理解是i个不同元素节点组成的二叉搜索树的个数为dp[i] ,都是一样的。
  2. 确定递推公式

    • dp[i] += dp[以j为头结点左子树节点数量] * dp[以j为头结点右子树节点数量]
    • j相当于是头结点的元素,从1遍历到i为止。
    • 所以递推公式:dp[i] += dp[j - 1] * dp[i - j]; ,j-1 为j为头结点左子树节点数量,i-j 为以j为头结点右子树节点数量
  3. dp数组如何初始化

    • 初始化,只需要初始化dp[0]就可以了,推导的基础,都是dp[0]。从定义上来讲,空节点也是一棵二叉树,也是一棵二叉搜索树,这是可以说得通的。
    • 所以初始化dp[0] = 1
  4. 确定遍历顺序

for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= i; j++) {
        dp[i] += dp[j - 1] * dp[i - j];
    }
}
- 首先一定是遍历节点数,从递归公式:dp[i] += dp[j - 1] * dp[i - j]可以看出,节点数为i的状态是依靠 i之前节点数的状态。那么遍历i里面每一个数作为头结点的状态,用j来遍历。
- 代码如下:
  1. 举例推导dp数组

代码:

class Solution {
    public int numTrees(int n) {
        // 1. 建立dp数组
        int[] dp = new int[n+1];
        // 2. 初始化dp数组
        dp[0] = 1;
        // 3. 根据公式递推
        for(int i = 1;i <= n;i++) {// 外层循环控制二叉树的总结点/总值
            for(int j = 1;j <= i;j++) {// 内层循环控制二叉树的根节点/值
                dp[i] += dp[j-1] * dp[i-j];
            }
        }
        // 4. 返回
        return dp[n];
    }
}

* 动态规划:01背包理论基础

https://kamacoder.com/problempage.php?pid=1046

题目描述

小明是一位科学家,他需要参加一场重要的国际科学大会,以展示自己的最新研究成果。他需要带一些研究材料,但是他的行李箱空间有限。这些研究材料包括实验设备、文献资料和实验样本等等,它们各自占据不同的空间,并且具有不同的价值。

小明的行李空间为 N,问小明应该如何抉择,才能携带最大价值的研究材料,每种研究材料只能选择一次,并且只有选与不选两种选择,不能进行切割。

输入描述

第一行包含两个正整数,第一个整数 M 代表研究材料的种类,第二个正整数 N,代表小明的行李空间。

第二行包含 M 个正整数,代表每种研究材料的所占空间。

第三行包含 M 个正整数,代表每种研究材料的价值。

输出描述

输出一个整数,代表小明能够携带的研究材料的最大价值。

输入示例

6 1
2 2 3 1 5 2
2 3 1 5 4 3

输出示例

5

提示信息

小明能够携带 6 种研究材料,但是行李空间只有 1,而占用空间为 1 的研究材料价值为 5,所以最终答案输出 5。

数据范围:
1 <= N <= 5000
1 <= M <= 5000
研究材料占用空间和价值都小于等于 1000

思路: 动态规划

  1. 确定dp数组以及下标的含义
    • 因为有两个维度需要分别表示:物品 和 背包容量dp[i] [j], i 来表示物品、j表示背包容量。
  2. 确定递推公式

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

- **不放物品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得到的最大价值
  1. dp数组如何初始化
    • 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物品
  2. 确定遍历顺序
for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= i; j++) {
        dp[i] += dp[j - 1] * dp[i - j];
    }
}
- 首先一定是遍历节点数,从递归公式:dp[i] += dp[j - 1] * dp[i - j]可以看出,节点数为i的状态是依靠 i之前节点数的状态。那么遍历i里面每一个数作为头结点的状态,用j来遍历。
- 代码如下:
  1. 举例推导dp数组

代码:

  • 二维数组
import java.util.Scanner;

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

        int[] weight = new int[n];
        int[] value = 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();
        
        // 2. 创建dp数组. 标号0-(n-1)的材料, 0-bagweight的最大可装重量
        int[][] dp = new int[n][bagweight+1];
        // 3. 初始化, 第一行, 标号为0的材料对bagweight, 只有可装>=bagweight才能初始化为0材料的价值
        for(int i = weight[0];i <= bagweight;i++) dp[0][i] = value[0];
        // 4. 根据公式填充dp数组
        for(int i = 1;i < n;i++) {// 从1号材料开始遍历到n-1号
            for(int j = 0;j <= bagweight;j++) {// 遍历每一种允许的weight
                if(j < weight[i]) {// 当前可装weight不能放入i号材料
                    dp[i][j] = dp[i-1][j];// 与装到i-1号材料不超过j重量的值一致
                }else {// 当前可装i号材料, 则在装和不装中选择一个value最大的
                    dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-weight[i]] + value[i]);
                }
            }
        }
        // 5. 打印
        System.out.print(dp[n-1][bagweight]);
    }
}
  • 一维数组
import java.util.Scanner;
public class Main{
    public static void main(String[] args) {
        // 1. 输入数据
        Scanner sc = new Scanner(System.in);
        int N = sc.nextInt();// 总材料种类
        int bagweight = sc.nextInt();// 行李箱空间(能装材料最大值)
        
        int[] costs = new int[N];// 材料所占空间
        int[] values = new int[N];// 材料价值

        // 2. 初始化
        for(int i = 0;i < N;i++) costs[i] = sc.nextInt();
        for(int i = 0;i < N;i++) values[i] = sc.nextInt();
        // 3. 创建dp数组, 保存0-bagweight行李箱所能装的材料价值
        int[] dp = new int[bagweight+1];
        // 4. 根据递推公式遍历
        for(int i = 0;i < N;i++) {// 遍历每种材料(二维数组每行)
            for(int j = bagweight;j >= costs[i];j--) {
                // 行李箱每种大小(倒序, 防止后面的值被前面已经改变的值影响)
                dp[j] = Math.max(dp[j],dp[j-costs[i]] + values[i]);
            }
        }
        // 5. 输出
        System.out.print(dp[bagweight]);
        sc.close();
    }
}

倒序遍历是为了保证物品i只被放入一次!。但如果一旦正序遍历了,那么物品0就会被重复加入多次!

举一个例子:物品0的重量weight[0] = 1,价值value[0] = 15

如果正序遍历

dp[1] = dp[1 - weight[0]] + value[0] = 15

dp[2] = dp[2 - weight[0]] + value[0] = 30

此时dp[2]就已经是30了,意味着物品0,被放入了两次,所以不能正序遍历。

为什么倒序遍历,就可以保证物品只放入一次呢?

倒序就是先算dp[2]

dp[2] = dp[2 - weight[0]] + value[0] = 15 (dp数组已经都初始化为0)

dp[1] = dp[1 - weight[0]] + value[0] = 15

所以从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了。

那么问题又来了,为什么二维dp数组遍历的时候不用倒序呢?

因为对于二维dp,dp[i][j]都是通过上一层即dp[i - 1][j]计算而来,本层的dp[i][j]并不会被覆盖!

416.分割等和子集

416. 分割等和子集

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

思路: 看作01背包问题, nums[i]对应的数值既是 重量 也是 价值, 背包大小为sum/2, 即平分总重量.

代码:

class Solution {
    public boolean canPartition(int[] nums) {// nums[i]代表物品i的开销和价值
        // 1. 转化nums数组, 取动态规划所需要的数值
        int n = nums.length;// 物品的种类
        int sum = 0;// 所有物品价值和
        for(int i : nums) sum+=i;
        // 2. 特殊判断
        if(sum % 2 != 0) return false;
        // 3. 初始化
        int target = sum/2;// 背包大小(要装入的价值)
        int[] dp = new int[target+1];// 每个背包大小所能装入的价值
        // 4. 根据迭代公式进行遍历
        for(int i = 0;i < n;i++) {// 0-(n-1)种物品
            for(int j = target;j >= nums[i];j--) {
                // 背包大小 从target-第i种物品所需的价值 所装入的价值
                dp[j] = Math.max(dp[j],dp[j-nums[i]] + nums[i]);
            }
            // 5. 返回(剪枝), 每层内循环后判断是否可以结束
            if(target == dp[target]) return true;
        }
        // 5. 返回
        return false;
    }
}

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

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

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

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

  • 如果 x == y,那么两块石头都会被完全粉碎;
  • 如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x

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

思路: 转换为动规01背包问题, 目标背包大小为总大小的一半(此时两堆重量最接近, 相减差值最小), 看目标大小的背包最多装多少物品, 用剩下的减去装入的即为所求.

代码:

class Solution {
    public int lastStoneWeightII(int[] stones) {
        // 1. 计算背包重量target(要达成的第一堆石头的重量)
        int sum = 0;
        for(int n : stones) sum+=n;
        int target = sum >> 1;
        // 2. 创建dp数组
        int[] dp = new int[target+1];
        // 3. 初始化: 0
        // 4. 根据递归公式循环遍历
        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]);
            }
        }
        // 5. 返回
        return sum - 2*dp[target];// 剩下的一堆: sum-dp[target] >= 分出来的一堆: dp[target]
    }
}

494.目标和 ?

494. 目标和

给你一个非负整数数组 nums 和一个整数 target

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

  • 例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1"

返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        // 1. 计算目标要装的bagSize
        // l - r = target, l + r = sum => l = (target + sum) / 2 即bagSize
        int sum = 0;

        for(int n : nums) sum+=n;
        // 特殊判断
        if(Math.abs(target) > sum) return 0;
        if((target+sum) % 2 == 1) return 0;
        // 计算bagsize
        int bagSize = (target + sum) / 2;
        // 建立dp数组, 装满大小为i的背包共有dp[i]种方法
        int[] dp = new int[bagSize+1];
        // 装满大小为0的背包共有1种方法
        dp[0] = 1;
        for(int i = 0;i < nums.length;i++) {
            for(int j = bagSize;j >= nums[i];j--) {
                dp[j]+=dp[j-nums[i]];// 放入当前: dp[2] = dp[1] +(放入当前) 
            }
        }

        return dp[bagSize];
    }
}

474.一和零

474. 一和零

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

请你找出并返回 strs 的最大子集的长度,该子集中 最多m0n1

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

思路: 二维重量的0-1背包问题

  1. 确定dp数组(dp table)以及下标的含义dp[i] [j]:最多有i个0和j个1的strs的最大子集的大小为dp[i] [j]
  2. 确定递推公式dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
  3. dp数组如何初始化01背包的dp数组初始化为0就可以。因为物品价值不会是负数,初始为0,保证递推的时候dp[i][j]不会被初始值覆盖。
  4. 确定遍历顺序01背包一定是: 外层for循环遍历物品,内层for循环遍历背包容量, 且从后向前遍历

代码:

class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        // 1. 建立dp数组,i:装入0的背包大小为i,j:装入1的背包大小为j,dp[i][j]这么大的背包装入字符串的个数
        int[][] dp = new int[m+1][n+1];
        // 2. 初始化. dp[0][0] = 0, 其余初始化为0(个数最小值), 防止Math.max被影响
        // 3. 根据迭代公式进行遍历
        // 3.1 外层遍历物品
        for(String str : strs) {
            // 统计每个字符串0 1个数, 即两维度要装入背包的物品大小 
            int zero = 0, one = 0;

            for(char c : str.toCharArray()) {
                if('0' == c) zero++;
                else one++;
            }
            // 3.2 内层遍历背包
            for(int i = m;i >= zero;i--) {// 外层维度遍历该字符串0的重量
                for(int j = n;j >= one;j--) {// 内层维度遍历该字符串1的重量
                    dp[i][j] = Math.max(dp[i][j],dp[i-zero][j-one] + 1);
                    //                不加改字符串 加该字符串
                }
            }
        }
        // 4. 返回
        return dp[m][n];
    }
}

* 完全背包理论基础-二维DP数组

理论讲解: https://programmercarl.com/%E8%83%8C%E5%8C%85%E9%97%AE%E9%A2%98%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80%E5%AE%8C%E5%85%A8%E8%83%8C%E5%8C%85.html#%E5%AE%8C%E5%85%A8%E8%83%8C%E5%8C%85

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

52.携带研究材料(第七期模拟笔试)

思路:

  1. 确定dp数组以及下标的含义dp[i] [j] 表示从下标为[0-i]的物品,每个物品可以取无限次,放进容量为j的背包,价值总和最大是多少。
  2. 确定递推公式dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]);
    • 不放物品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得到的最大价值
  3. dp数组如何初始化状态转移方程 可以看出有一个方向 i 是由 i-1 推导出来,那么i为0的时候就一定要初始化。
    • 那么很明显当 j < weight[0]的时候,dp[0] [j] 应该是 0,因为背包容量比编号0的物品重量还小。
    • j >= weight[0]时,dp[0] [j] 如果能放下weight[0]的话,就一直装,每一种物品有无限个
    • 其他下标初始为什么数值都可以,因为都会被覆盖。但只不过一开始就统一把dp数组统一初始为0,更方便一些。
// 初始化 dp
vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 0));
for (int j = weight[0]; j <= bagWeight; j++) {
    dp[0][j] = dp[0][j - weight[0]] + value[0]; 
}
  1. 确定遍历顺序先物品后背包 或 先背包后物品 均可.两种遍历顺序,对于二维dp数组来说,递推公式所需要的值,二维dp数组里对应的位置都有。
  2. 举例推导dp数组

代码:

import java.util.Scanner;

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

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

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

        // 1. 创建dp数组,dp[i][j]:重量为j的背包放入物品0-i的最大价值
        int[][] dp = new int[n][bagweight+1];
        // 2. 初始化
        for(int j = weight[0];j <= bagweight;j++) {
            dp[0][j] = dp[0][j-weight[0]] + value[0];// 放入0号物品
        }
        
        // 3. 根据递推公式遍历填充
        for(int i = 1;i < n;i++) {// 外层:遍历物品1-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.print(dp[n-1][bagweight]);
        sc.close();
    }
}

518.零钱兑换 II

518. 零钱兑换 II

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

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

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

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

**思路: ** 完全背包问题.

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

     dp[j]:凑成总金额j的货币组合数为dp[j]
    
  2. 确定递推公式

     本题 二维dp 递推公式: `dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i]]`
    
     压缩成一维:`dp[j] += dp[j - coins[i]]`
    
  3. dp数组如何初始化

     装满背包容量为0 的方法是1,即不放任何物品,`dp[0] = 1`
    
  4. 确定遍历顺序外物品内背包: 组合数 外背包内物品: 排列数

     纯完全背包求得装满背包的最大价值是多少,和凑成总和的元素有没有顺序没关系,即:有顺序也行,没有顺序也行, 而本题要求凑成总和的组合数,元素之间明确要求<font style="background-color:#f3bb2f;">没有顺序</font>。
    
     本题是求凑出来的方案个数,且每个方案个数是<font style="background-color:#f3bb2f;">组合数</font>。
    
  5. 举例推导dp数组

代码:

class Solution {
    public int change(int amount, int[] coins) {
        // 1. 建立dp数组.dp[i]:大小为i的背包 用coins中的硬币凑成amount(背包总重量) 有dp[i]种组合方法
        int[] dp = new int[amount+1];
        // 2. 初始化
        dp[0] = 1;
        // 3. 根据递推公式遍历
        for(int i = 0;i < coins.length;i++) {
            // 外物品:固定物品的出现顺序, 确保最后统计的是组合数而不是排列数
            for(int j = coins[i];j <= amount;j++) {
                // 内背包:累加不同组合
                dp[j] += dp[j-coins[i]];
            }
        }
        // 4. 返回
        return dp[amount];
    }
}

377.组合总和 Ⅳ

377. 组合总和 Ⅳ

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

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

**思路: ** 完全背包全排列

代码:

class Solution {
    public int combinationSum4(int[] nums, int target) {
        // 1. 建立dp
        int[] dp = new int[target+1];
        // 2. 初始化
        dp[0] = 1;
        // 3. 根据递推公式迭代
        for(int i = 0;i <= target;i++) {// 背包(0-target)
            for(int j = 0;j < nums.length;j++) {// 物品(整个nums)
                if(i >= nums[j]) dp[i] += dp[i-nums[j]];// 背包够装
            }
        }  
        // 4. 返回
        return dp[target];
    }
}

70. 爬楼梯(进阶版)

假设你正在爬楼梯。需要 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 阶

**思路: ** 完全背包全排列问题

代码:

import java.util.Scanner;
public class Main{
    public static void main(String[] args) {
        // 1. 输入
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();// 背包
        int m = sc.nextInt();// 物品
        // 2. 建立dp数组
        int[] dp = new int[n+1];
        // 3. 初始化: 为了后序累加, 必须为1不为0
        dp[0] = 1;
        // 4. 根据迭代公式遍历
        for(int i = 1;i <= n;i++) {// 外背包
            for(int j = 1;j <= m;j++) {// 内物品(楼梯每次1-m个)
                if(i >= j) dp[i] += dp[i-j];
            }
        }

        System.out.print(dp[n]);
        sc.close();
    }
}

322.零钱兑换

322. 零钱兑换

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

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

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

**思路: ** 完全背包(组合数)

代码:

class Solution {
    public int coinChange(int[] coins, int amount) {
        // 1. 建立dp数组
        int[] dp = new int[amount+1];
        // 2. 初始化
        int max = Integer.MAX_VALUE;
        Arrays.fill(dp,max);// 最大值(防止影响取min)
        dp[0] = 0;// 总金额为0共0种
        // 3. 根据递推公式进行遍历
        for(int i = 0;i < coins.length;i++) {// 物品
            for(int j = coins[i];j <= amount;j++) {// 背包
                if(dp[j-coins[i]] != max) dp[j] = Math.min(dp[j-coins[i]] + 1,dp[j]);
            }
        }
        // 4. 返回
        return dp[amount] == max ? -1 : dp[amount];
    }
}

279.完全平方数

279. 完全平方数

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

完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,14916 都是完全平方数,而 311 不是。

思路: 完全背包, 求种类最小数值.

代码:

class Solution {
    public int numSquares(int n) {
        // 1. 建立dp数组
        int[] dp = new int[n+1];
        // 2. 初始化
        for(int i = 0;i <= n;i++) {
            dp[i] = Integer.MAX_VALUE;
        }
        dp[0] = 0;
        // 3. 根据递推公式迭代
        for(int i = 1;i <= n;i++) {// 外物品
            for(int j = i*i;j <= n;j++) {// 内背包(完全平方数dp[i] == i*i) 
                dp[j] = Math.min(dp[j],dp[j-i*i] + 1);
                // 因为有1, 所以每个背包一定有解, 不需要是否为max的判断来避免加一溢出
            }
        }
        // 4. 返回
        return dp[n];
    }
}

139.单词拆分

139. 单词拆分

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true

**注意:**不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。

**思路: ** 完全背包, 排列数, 是否可以

代码:

class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        // 1. 建立dp数组, dp[i]:长度为i的字符串是否能由wordDict组成
        boolean[] dp = new boolean[s.length()+1];
        // 2. 初始化, 为了后序遍历不全为false, 与dp[0]有关
        dp[0] = true;
        // 3. 根据迭代公式循环遍历(题目要求与顺序有关,因此外背包,内物品)
        Set<String> set = new HashSet<>(wordDict);// 去重收集, 提高效率
        for(int i = 1;i <= s.length();i++) {
            for(int j = 0;j < i && !dp[i];j++) {
                // 当出现dp[i] == true就不需要向后遍历, 后序不会再改变该值
                if(set.contains(s.substring(j,i)) && dp[j]) dp[i] = true;
                // 必须在dp[j] == true的时候才能保证dp[i] == true;
            }
        }
        // 4. 返回
        return dp[s.length()];
    }
}

* 多重背包

https://programmercarl.com/%E8%83%8C%E5%8C%85%E9%97%AE%E9%A2%98%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80%E5%A4%9A%E9%87%8D%E8%83%8C%E5%8C%85.html#%E5%A4%9A%E9%87%8D%E8%83%8C%E5%8C%85

  1. 携带矿石资源(第八期模拟笔试)

https://kamacoder.com/problempage.php?pid=1066

思路: 完全背包. 把每种商品遍历的个数放在01背包里面在遍历一遍

代码:

import java.util.Scanner;
public class Main {
    public static void main(String[] args) {
        // 1. 输入
        Scanner sc = new Scanner(System.in);
        int bagweight = sc.nextInt();
        int n = sc.nextInt();

        int[] weight = new int[n];
        int[] value = new int[n];
        int[] num = 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++) num[i] = sc.nextInt();
        // 2. 创建dp数组
        int[] dp = new int[bagweight+1];// 总价值
        for(int i = 0;i < n;i++) {// 物品
            for(int j = bagweight;j >= weight[i];j--) {// 背包
                for(int k = 1;k <= num[i] && j >= k*weight[i];k++) {// 物品个数
                    dp[j] = Math.max(dp[j],dp[j-k*weight[i]] + k*value[i]);
                }
            }
        }
        // 3. 打印
        System.out.print(dp[bagweight]);
    }
}

背包问题总结

1. 几种常见背包

2. 动规解题五部曲

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

3. 背包递推公式

问能否能装满背包(或者最多装多少):dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]); ,对应题目如下:

  • 动态规划:416.分割等和子集(opens new window)
  • 动态规划:1049.最后一块石头的重量 II(opens new window)

问装满背包有几种方法:dp[j] += dp[j - nums[i]] ,对应题目如下:

  • 动态规划:494.目标和(opens new window)
  • 动态规划:518. 零钱兑换 II(opens new window)
  • 动态规划:377.组合总和Ⅳ(opens new window)
  • 动态规划:70. 爬楼梯进阶版(完全背包)(opens new window)

问背包装满最大价值:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); ,对应题目如下:

  • 动态规划:474.一和零(opens new window)

问装满背包所有物品的最小个数:dp[j] = min(dp[j - coins[i]] + 1, dp[j]); ,对应题目如下:

  • 动态规划:322.零钱兑换(opens new window)
  • 动态规划:279.完全平方数

4. 遍历顺序

01背包

在动态规划:关于01背包问题,你该了解这些! (opens new window)中我们讲解二维dp数组01背包先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。

和动态规划:关于01背包问题,你该了解这些!(滚动数组) (opens new window)中,我们讲解一维dp数组01背包只能先遍历物品再遍历背包容量,且第二层for循环是从大到小遍历。

#完全背包

在动态规划:关于完全背包,你该了解这些! (opens new window)中,讲解了纯完全背包的一维dp数组实现,先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。

但是仅仅是纯完全背包的遍历顺序是这样的,题目稍有变化,两个for循环的先后顺序就不一样了。

如果求组合数就是外层for循环遍历物品,内层for遍历背包

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

相关题目如下:

  • 求组合数:动态规划:518.零钱兑换II(opens new window)
  • 求排列数:动态规划:377. 组合总和 Ⅳ (opens new window)、动态规划:70. 爬楼梯进阶版(完全背包)(opens new window)

如果求最小数,那么两层for循环的先后顺序就无所谓了,相关题目如下:

  • 求最小数:动态规划:322. 零钱兑换 (opens new window)、动态规划:279.完全平方数(opens new window)

198.打家劫舍

198. 打家劫舍

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

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

思路: 动态规划.

代码:

class Solution {
    public int rob(int[] nums) {
        // 0. 特殊判断, 防止dp[1]取值时nums[1]下标越界
        if(nums.length == 1) return nums[0];

        // 1. 建立dp数组
        int[] dp = new int[nums.length];// dp[i]:考虑到nums[i]所偷窃的最大金额
        
        // 2. 初始化
        dp[0] = nums[0];
        dp[1] = Math.max(nums[0],nums[1]);

        // 3. 根据递推公式迭代遍历
        for(int i = 2;i < nums.length;i++) {
            dp[i] = Math.max(dp[i-1],dp[i-2] + nums[i]);
            //           不加nums[i]  加nums[i]
        }

        // 4. 返回
        return dp[nums.length-1];
    }
}

213.打家劫舍 II

213. 打家劫舍 II

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

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

思路: 动态规划. 根据打家劫舍I, 将循环拆成两个顺序数组, 分别传入打家劫舍I的代码中, 取最大值.

![](https://gitee.com/kiddkid/images/raw/master/202503112155552.jpeg)![](https://gitee.com/kiddkid/images/raw/master/202503112155315.jpeg)

代码:

class Solution {
    public int rob(int[] nums) {
        // 1. 特殊判断, 保证nums.length >= 2, 使下方传入doRob的参数合法
        if(nums.length == 1) return nums[0];
        // 2. 返回 0 ~ nums.length - 2, 1 ~ nums.length - 1 数组顺序遍历无循环情况下的最大价值
        return Math.max( doRob(nums,0,nums.length - 2), doRob(nums,1,nums.length - 1) );
    }
    // 真正执行打家劫舍的算法, 顺序打家劫舍
    public int doRob(int[] nums,int start,int end) {
        // 1. 创建dp数组
        int[] dp = new int[end - start + 1];
        // 2. 初始化
        dp[0] = nums[start];
        // 防止start == end, start+1 > end, 操作范围内下标越界
        if(start + 1 <= end) dp[1] = Math.max(nums[start],nums[start + 1]);
        // 3. 根据递推公式迭代遍历
        for(int i = start + 2;i <= end;i++) {// 以start为对照点计算dp数组的下标, 确保dp数组从0开始顺序填充
            dp[i - start] = Math.max(dp[i - start - 1], dp[i - start - 2] + nums[i]);
        }
        // 4. 返回
        return dp[end - start];
    }
}

337.打家劫舍 III

337. 打家劫舍 III

小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root

除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。

给定二叉树的 root 。返回 在不触动警报的情况下_ ,小偷能够盗取的最高金额_ 。

**思路: ** 树形dp

1. 确定递归函数的参数和返回值

这里我们要求一个节点 偷与不偷的两个状态所得到的金钱,那么返回值就是一个长度为2的数组。

参数为当前节点,代码如下:

int[] doRob(TreeNode* cur) { ... }

其实这里的返回数组就是dp数组。

所以dp数组(dp table)以及下标的含义:下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱。

2. 确定终止条件

在遍历的过程中,如果遇到空节点的话,很明显,无论偷还是不偷都是0,所以就返回

if (cur == NULL) return int[] {0, 0};

这也相当于dp数组的初始化

3. 确定遍历顺序

使用后序遍历。 因为要通过递归函数的返回值来做下一步计算。

通过递归左节点,得到左节点偷与不偷的金钱。

通过递归右节点,得到右节点偷与不偷的金钱。

代码如下:

// 下标0:不偷,下标1:偷
vector<int> left = robTree(cur->left); // 左
vector<int> right = robTree(cur->right); // 右
// 中

4. 确定单层递归的逻辑

如果是偷当前节点,那么左右孩子就不能偷,val1 = cur->val + left[0] + right[0]; (如果对下标含义不理解就再回顾一下dp数组的含义

如果不偷当前节点,那么左右孩子就可以偷,至于到底偷不偷一定是选一个最大的,所以:val2 = max(left[0], left[1]) + max(right[0], right[1]);

最后当前节点的状态就是{val2, val1}; 即:{不偷当前节点得到的最大金钱,偷当前节点得到的最大金钱}

代码如下:

vector<int> left = robTree(cur->left); // 左
vector<int> right = robTree(cur->right); // 右

// 偷cur
int val1 = cur->val + left[0] + right[0];
// 不偷cur
int val2 = max(left[0], left[1]) + max(right[0], right[1]);
return {val2, val1};

5. 举例推导dp数组

以示例1为例,dp数组状态如下:(注意用后序遍历的方式推导

最后头结点就是 取下标0 和 下标1的最大值就是偷得的最大金钱。

代码:

class Solution {
    public int rob(TreeNode root) {
        int[] res = doRob(root);// 获取根节点不偷和偷的情况下获取金额
        return Math.max(res[0],res[1]);// 返回偷和不偷的最大值
    }
    // 开始打家劫舍
    public int[] doRob(TreeNode root) {
        // 1. 递归出口: 当前为结点为null, 当前节点偷不偷最大值都为0
        int[] res = new int[2];
        if(root == null) return res;

        // 2. 递归主要逻辑: 后序遍历
        // 2.1 左
        int[] leftDp = doRob(root.left);
        // 2.2 右
        int[] rightDp = doRob(root.right);
        // 2.3 中
        // 2.3.1 不偷当前节点: 左偷不偷最大值 + 右偷不偷最大值
        res[0] = Math.max(leftDp[0],leftDp[1]) + Math.max(rightDp[0],rightDp[1]);
        // 2.3.2 偷当前节点: 左右均不偷 + 当前节点的价值
        res[1] = leftDp[0] + rightDp[0] + root.val;

        // 3. 返回结果: 偷不偷最大值组成的dp数组
        return res;    
    }
}

121.买卖股票的最佳时机

121. 买卖股票的最佳时机

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

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

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

思路: 动态规划

  • 确定dp数组(dp table)以及下标的含义
    • dp[i] [0] 表示第i天持有股票所得最多现金
    • 注意这里说的是“持有”,“持有”不代表就是当天“买入”!也有可能是昨天就买入了,今天保持持有的状态
    • dp[i] [1] 表示第i天不持有股票所得最多现金

代码:

class Solution {
    public int maxProfit(int[] prices) {
        // 1. 建立dp数组
        int len = prices.length;
        int[][] dp = new int[len][2];// 第i天持有/不持有该股票所获的最大利润
        
        // 2. 初始化
        dp[0][0] = -prices[0];// 持有:第一天购买
        dp[0][1] = 0;// 不持有

        // 3. 根据递推公式迭代遍历
        for(int i = 1;i < len;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]);// 之前就不持有 | 当前不持有
        }

        // 4. 返回
        return dp[len-1][1];// 该题一定是不持有 > 持有
    }
}

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

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

给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。

在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。

返回 你能获得的 最大 利润

**思路: ** 同121, 注意购入时本金变化, 由于可以多次买卖, 购入时本金不一定为0了

代码:

class Solution {
    public int maxProfit(int[] prices) {
        // 1. 创建dp数组
        int len = prices.length;
        int[][] dp = new int[len][2];
        // 2. 初始化
        dp[0][0] = -prices[0];// 持有
        dp[0][1] = 0;// 不持有
        // 3. 根据递推公式循环遍历
        for(int i = 1;i < len;i++) {
            // 之前就持有 | 之前不持有, 现在持有, 由于可以买卖多次, 当前不一定是第一次, 故要用dp[i-1][1] - prices[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]);
        }
        // 4. 返回
        return dp[len-1][1];// 不持有 > 持有
    }
}

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

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

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

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

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

思路: 动规, 定义五种状态确保统计两次买入两次卖出的情况.

代码:

class Solution {
    public int maxProfit(int[] prices) {
        // 1. 建立dp数组
        int len = prices.length;
        int[][] dp = new int[len][5];
        // 定义五种状态, 0:无操作 1:第一次买入 2:第一次卖出 3:第二次买入 4:第二次卖出

        // 2. 初始化
        dp[0][1] = -prices[0];
        dp[0][3] = -prices[0];

        // 3. 根据递推公式进行遍历
        for(int i = 1;i < len;i++) {
            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]);
        }

        // 4. 返回
        return dp[len-1][4];
    }
}

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

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

给你一个整数数组 prices 和一个整数 k ,其中 prices[i] 是某支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。也就是说,你最多可以买 k 次,卖 k 次。

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

思路: 动态规划. 将状态根据循环进行抽象.

代码:

class Solution {
    public int maxProfit(int k, int[] prices) {
        // 1. 建立dp数组
        int len = prices.length;
        int[][] dp = new int[len][2*k+1];// 0~len,0~2k

        // 2. 初始化: 只有奇数次卖出的时候初始化为-prices[0], 其余均为0
        for(int i = 1;i < 2*k;i+=2) dp[0][i] = -prices[0];

        // 3. 根据递推公式遍历
        for(int i = 1;i < len;i++) {// prices下标i
            for(int j = 0;j < 2*k;j+=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]);
            }
        }

        // 4. 返回
        return dp[len-1][2*k];
    }
}

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

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

给定一个整数数组prices,其中第 prices[i] 表示第 *i* 天的股票价格 。

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

  • 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。

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

思路: 动态规划.

122基础上定义冷冻期, 定义四种状态: [持有, 卖出, 冷冻期, 保持卖出 … ]

  • 0: 持有
  • 1: 保持卖出
  • 2: 卖出当天
  • 3: 冷冻期

代码:

class Solution {
    public int maxProfit(int[] prices) {
        // 1. 建立dp数组
        int len = prices.length;
        int[][] dp = new int[len][4];
        // 2. 初始化dp数组
        dp[0][0] = -prices[0];
        // 3. 根据递推公式遍历
        for(int i = 1;i < len;i++) {
            // 
            dp[i][0] = Math.max(dp[i-1][0], Math.max(dp[i-1][1] - prices[i], dp[i-1][3] - prices[i]));
            dp[i][1] = Math.max(dp[i-1][1], dp[i-1][3]);
            dp[i][2] = dp[i][0] + prices[i];
            dp[i][3] = dp[i-1][2];
        }
        // 4. 返回
        return Math.max(dp[len-1][1], Math.max(dp[len-1][2], dp[len-1][3]));
    }
}

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

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

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

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

返回获得利润的最大值。

**注意:**这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。

思路: 在买卖股票II的基础上添加手续费.

代码:

class Solution {
    public int maxProfit(int[] prices, int fee) {
        // 1. dp
        int n = prices.length;
        int[][] dp = new int[n][2];
        // 2. 初始化
        dp[0][0] = -prices[0];
        // 3. 遍历
        for(int i = 1;i < n;i++) {
            dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] - prices[i]);
            dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] + prices[i] - fee);
            // 一次交易:买入-持有-卖出, 卖出时支付手续费
        }
        // 4. 返回
        return Math.max(dp[n-1][0], dp[n-1][1]);
    }
}

300.最长递增子序列

300. 最长递增子序列

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

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

**思路: ** 动规.

  1. dp[i]的定义: dp[i]表示i之前包括i的以nums[i]结尾的最长递增子序列的长度

  2. 状态转移方程: 位置i的最长升序子序列等于j从0到i-1各个位置的最长升序子序列 + 1 的最大值。if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);注意这里不是要dp[i] 与 dp[j] + 1进行比较,而是我们要取dp[j] + 1的最大值。

  3. dp[i]的初始化: 每一个i,对应的dp[i](即最长递增子序列)起始大小至少都是1.

  4. 确定遍历顺序: dp[i] 是有0到i-1各个位置的最长递增子序列 推导而来,那么遍历i一定是从前向后遍历。j其实就是遍历0到i-1,那么是从前到后,还是从后到前遍历都无所谓,只要吧 0 到 i-1 的元素都遍历了就行了。 所以默认习惯 从前向后遍历。

     遍历i的循环在外层,遍历j则在内层,代码如下:
    
for (int i = 1; i < nums.size(); i++) {
    for (int j = 0; j < i; j++) {
        if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);
    }
    if (dp[i] > result) result = dp[i]; // 取长的子序列
}
  1. 举例推导dp数组

输入:[0,1,0,3,2],dp数组的变化如下:

代码:

class Solution {
    public int lengthOfLIS(int[] nums) {
        // 1. dp数组
        int n = nums.length;
        int[] dp = new int[n];// i之前包括i的以nums[i]结尾的最长递增子序列的长度

        // 2. 初始化
        Arrays.fill(dp,1);// 至少有nums[i], 故初始为1

        // 3. 根据递推公式遍历
        int res = 1;
        for(int i = 1;i < n;i++) {// 当前遍历到的位置
            for(int j = 0;j < i;j++) {// 0~i, 前面的可以不相邻
                if(nums[i] > nums[j]) dp[i] = Math.max(dp[i], dp[j] + 1);
            }
            res = Math.max(res, dp[i]);// 找出最大的dp[i]
        }

        // 4. 返回
        return res;
    }
}

674.最长连续递增序列

674. 最长连续递增序列

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

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

**思路: ** 动规. 类似300.

代码:

class Solution {
    public int findLengthOfLCIS(int[] nums) {
        // 1. dp
        int n = nums.length;
        int[] dp = new int[n];

        // 2. 初始化
        Arrays.fill(dp, 1);

        // 3. 根据递推公式遍历
        int res = 1;
        for(int i = 1;i < n;i++) {
            if(nums[i - 1] < nums[i]) {
                // 注意: 比较的是nums里的数值, 由于连续, 只需要相邻比较
                dp[i] = dp[i - 1] + 1;
            }
            if(dp[i] > res) res = dp[i];
        }

        // 4. 返回
        return res;
    }
}

718.最长重复子数组

718. 最长重复子数组

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

**思路: ** 二维dp数组

  1. 确定dp数组(dp table)以及下标的含义
    • dp[i] [j] :以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组长度为dp[i] [j]。 (特别注意: “以下标i - 1为结尾的A” 标明一定是 以A[i-1]为结尾的字符串 )
    • 其实dp[i] [j]的定义也就决定着,我们在遍历dp[i][j]的时候i 和 j都要从1开始。
  2. 确定递推公式
    • 根据dp[i] [j]的定义,dp[i] [j]的状态只能由dp[i - 1] [j - 1]推导出来。
    • 即当A[i - 1] 和B[j - 1]相等的时候,dp[i] [j] = dp[i - 1] [j - 1] + 1;
    • 根据递推公式可以看出,遍历i 和 j 要从1开始
  3. dp数组如何初始化
    • 根据dp[i] [j]的定义,dp[i] [0] 和dp[0] [j]其实都是没有意义的
    • 但dp[i] [0] 和dp[0] [j]要初始值,因为 为了方便递归公式dp[i] [j] = dp[i - 1] [j - 1] + 1;
    • 所以dp[i] [0] 和dp[0] [j]初始化为0。
  4. 确定遍历顺序
    • 外层for循环遍历A,内层for循环遍历B。(换过来也行)
    • 同时题目要求长度最长的子数组的长度。所以在遍历的时候顺便把dp[i] [j]的最大值记录下来。
for (int i = 1; i <= nums1.size(); i++) {
    for (int j = 1; j <= nums2.size(); j++) {
        if (nums1[i - 1] == nums2[j - 1]) {
            dp[i][j] = dp[i - 1][j - 1] + 1;
        }
        if (dp[i][j] > result) result = dp[i][j];
    }
}
  1. 举例推导dp数组

代码:

class Solution {
    public int findLength(int[] nums1, int[] nums2) {
        // 1. 建立dp数组    
        // dp[i][j]: nums1到i-1, nums2到j-1 两数组最长重复子数组, dp往后顺移一位, 因此长度需要加一
        int[][] dp = new int[nums1.length + 1][nums2.length + 1];

        // 2. 初始化:dp[i][0] == 0, dp[0][i] == 0  0-1无意义

        // 3. 根据递推公式遍历
        int res = 0;
        for(int i = 1;i <= nums1.length;i++) {// nums1
            for(int j = 1;j <= nums2.length;j++) {// nums2
                if(nums1[i-1] == nums2[j-1]) dp[i][j] = dp[i-1][j-1] + 1;
                // 根据nums1[i-1] num2[j-1]更新dp

                if(dp[i][j] > res) res = dp[i][j];// 统计最大的dp[i][j]
            }
        }

        // 4. 返回
        return res;
    }
}

1143.最长公共子序列

1143. 最长公共子序列

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

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

  • 例如,"ace""abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。

两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

**思路: ** 因为可以不连续, 当两个字符串当前字符不相等的时候还可以根据之前的结果继续统计.

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

代码:

class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        // 0. 把字符串转为字符串数组
        char[] s1 = text1.toCharArray();
        char[] s2 = text2.toCharArray();
        // 1. 创建dp
        // dp[0][0] ~ dp[s1.length][s2.length]: i~s1.length-1, j~s2.length-1
        int[][] dp = new int[s1.length + 1][s2.length + 1];
        // 2. 初始化: dp[0][j], dp[i][0] 均初始化为0
        // 3. 根据递推公式遍历
        for(int i = 1;i <= s1.length;i++) {// s1
            for(int j = 1;j <= s2.length;j++) {// s2
                if(s1[i-1] == s2[j-1]) {
                    dp[i][j] = dp[i-1][j-1] + 1;
                }else {// 当前字符不相等, 不需要+1
                    dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
                }
            }
        }
        // 4. 返回
        return dp[s1.length][s2.length];
    }
}

1035.不相交的线

1035. 不相交的线

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

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

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

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

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

思路: 不相交的线连接的就是两个数组例的最长公共子序列, 思路同1143, 代码同1143.

代码:

class Solution {
    public int maxUncrossedLines(int[] nums1, int[] nums2) {
        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];
    }
}

53.最大子数组和

53. 最大子数组和

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

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

思路: 动态规划.

  1. 确定dp数组(dp table)以及下标的含义

     **dp[i]:包括下标i(以nums[i]为结尾)的最大连续子序列和为dp[i]**。
    
  2. 确定递推公式

     dp[i]只有两个方向可以推出来:
    
  • dp[i - 1] + nums[i],即:nums[i]加入当前连续子序列和

  • nums[i],即:从头开始计算当前连续子序列和

    一定是取最大的,所以dp[i] = max(dp[i - 1] + nums[i], nums[i]);

  1. dp数组如何初始化

从递推公式可以看出来dp[i]是依赖于dp[i - 1]的状态,dp[0]就是递推公式的基础。

根据dp[i]的定义,很明显dp[0]应为nums[0]即dp[0] = nums[0]

  1. 确定遍历顺序

递推公式中dp[i]依赖于dp[i - 1]的状态,需要从前向后遍历。

  1. 举例推导dp数组

代码:

class Solution {
    public int maxSubArray(int[] nums) {
        // 1. 创建dp数组, dp[i]: 统计到nums[i]的最大子数组和 
        int[] dp = new int[nums.length];

        // 2. 初始化
        dp[0] = nums[0];

        // 3. 根据递推公式遍历
        int res = dp[0];
        for(int i = 1;i < nums.length;i++) {
            // 跟前面连接 | 从nums[i]开始
            dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]);
            // 统计dp[i]的最大值作为返回值
            if(dp[i] > res) res = dp[i];
        }

        // 4. 返回
        return res;
    }
}

392.判断子序列

392. 判断子序列

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

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

思路: 动态规划.

  1. 确定dp数组(dp table)以及下标的含义

     **dp[i] [j] 表示以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度为dp[i] [j]**。注意这里是判断s是否为t的子序列。即t的长度是大于等于s的。
    
  2. 确定递推公式

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

其实这里 大家可以发现和 1143.最长公共子序列 (opens new window)的递推公式基本那就是一样的,区别就是 本题 如果删元素一定是字符串t,而 1143.最长公共子序列 是两个字符串都可以删元素。

  1. dp数组如何初始化

dp[i] [j]是依赖于dp[i - 1] [j - 1] 和 dp[i] [j - 1],所以dp[0] [0]和dp[i] [0]是一定要初始化的。

  1. 确定遍历顺序

同理从递推公式可以看出dp[i] [j]都是依赖于dp[i - 1] [j - 1] 和 dp[i] [j - 1],那么遍历顺序也应该是从上到下,从左到右

如图所示:

  1. 举例推导dp数组

以示例一为例,输入:s = “abc”, t = “ahbgdc”,dp状态转移图如下:

dp[i] [j]表示以下标i-1为结尾的字符串s和以下标j-1为结尾的字符串t 相同子序列的长度,所以如果dp[s.size()] [t.size()] 与 字符串s的长度相同说明:s与t的最长相同子序列就是s,那么s 就是 t 的子序列。

图中dp[s.size()] [t.size()] = 3, 而s.size() 也为3。所以s是t 的子序列,返回true。

代码:

class Solution {
    public boolean isSubsequence(String s, String t) {
        int len1 = s.length(), len2 = t.length();
        // 1. 建立dp数组: dp[i][j] = 统计到s[i-1], t[j-1], t[j-1]所含s[i-1]的子序列
        int[][] dp = new int[len1 + 1][len2 + 1];

        // 2. 根据递推公式遍历
        for(int i = 1;i <= len1;i++) {
            for(int j = 1;j <= len2;j++) {
                if(s.charAt(i - 1) == t.charAt(j - 1)) {// s(i - 1) t(j - 1)
                    dp[i][j] = dp[i - 1][j - 1] + 1;// + 1
                }else dp[i][j] = dp[i][j - 1];
            }
        }

        // 3. 返回, 看len2与len1公共子序列的长度是否为len2
        return dp[len1][len2] == len1;
    }
}

115.不同的子序列

115. 不同的子序列

给你两个字符串 st ,统计并返回在 s子序列t 出现的个数,结果需要对 10^9 + 7 取模。

思路: 动规.

  1. 确定dp数组(dp table)以及下标的含义

     dp[i] [j]:以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]。
    
  2. 确定递推公式

  • s[i - 1] 与 t[j - 1]相等

  • s[i - 1] 与 t[j - 1] 不相等

      当s[i - 1] 与 t[j - 1]相等时,dp[i] [j]可以有两部分组成。
    
      一部分是用s[i - 1]来<font style="background-color:#f3bb2f;">匹配</font>,那么个数为dp[i - 1] [j - 1]。即不需要考虑当前s子串和t子串的最后一位字母,所以只需要 dp[i-1] [j-1]。
    
      一部分是不用s[i - 1]来匹配,个数为dp[i - 1] [j]。
    
  1. dp数组如何初始化

     从递推公式`dp[i][j] = dp[i - 1][j - 1] + dp[i - 1`][j]; 和 `dp[i][j] = dp[i - 1`][j]; 中可以看出dp[i] [j] 是从上方和左上方推导而来,如图:,那么 dp[i] [0] 和dp[0] [j]是一定要初始化的。
    

vector<vector<long long>> dp(s.size() + 1, vector<long long>(t.size() + 1));
for (int i = 0; i <= s.size(); i++) dp[i][0] = 1;
for (int j = 1; j <= t.size(); j++) dp[0][j] = 0; // 其实这行代码可以和dp数组初始化的时候放在一起,但我为了凸显初始化的逻辑,所以还是加上了。
  1. 确定遍历顺序

     从递推公式`dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]; 和 dp[i][j] = dp[i - 1`][j]; 中可以看出dp[i] [j]都是根据左上方和正上方推出来的。所以遍历的时候一定是从上到下,从左到右,这样保证dp[i] [j]可以根据之前计算出来的数值进行计算。
    

代码如下:

for (int i = 1; i <= s.size(); i++) {
    for (int j = 1; j <= t.size(); j++) {
        if (s[i - 1] == t[j - 1]) {
            dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
        } else {
            dp[i][j] = dp[i - 1][j];
        }
    }
}
  1. 举例推导dp数组

代码:

class Solution {
    public int numDistinct(String s, String t) {
        // 1. 建立dp数组
        int len1 = s.length(), len2 = t.length();
        int[][] dp = new int[len1 + 1][len2 + 1];
        // 2. 初始化, 根据递推公式, 所有值都从左上方, 上方递推而来, 需要初始化上方(dp[0][j] == 0), 左方
        // 左:
        for(int i = 0;i <= len1;i++) {
            dp[i][0] = 1;
        }
        // 3. 根据递推公式遍历
        for(int i = 1;i <= len1;i++) {
            for(int j = 1;j <= len2;j++) {
                // 当前位置两个字符串字符相同
                if(s.charAt(i - 1) == t.charAt(j - 1)) {
                    // 当前相同: 都去掉i, 之前字符串相同的个数 + 去掉s(i)之前字符串相同的个数
                    dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
                }else {
                    // 当前不同: 值为之前字符串相同的个数
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }
        // 4. 返回
        return dp[len1][len2];
    }
}

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

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

给定两个单词 word1word2 ,返回使得 word1word2 相同所需的最小步数

每步 可以删除任意一个字符串中的一个字符。

示例 1:

输入: word1 = "sea", word2 = "eat"
输出: 2
解释: 第一步将 "sea" 变为 "ea" ,第二步将 "eat "变为 "ea"

示例 2:

输入:word1 = "leetcode", word2 = "etco"
输出:4

提示:

  • 1 <= word1.length, word2.length <= 500
  • word1word2 只包含小写英文字母

**思路: ** 动规, 二维dp数组表示两个字符串.

代码:

class Solution {
    public int minDistance(String word1, String word2) {
        // 1. dp, dp[i][j]: 以i-1为结尾的字符串word1,和以j-1位结尾的字符串word2,想要达到相等,所需要删除元素的最少次数。
        int len1 = word1.length(), len2 = word2.length();
        int[][] dp = new int[len1 + 1][len2 + 1];

        // 2. 初始化: 
        for (int i = 0; i <= len1; i++)
            dp[i][0] = i;// dp[i][0]: 统计到下标为i-1, i: 元素个数为i, 共删除i次
        for (int j = 0; j <= len2; j++)
            dp[0][j] = j;

        // 3. 根据递推公式遍历
        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 - 1] + 2, Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1));
                    // 考虑到当前位置相同: 在之前已经相同的删除次数上 删除 该次需要删除的个数
                }
            }
        }

        // 4. 返回
        return dp[len1][len2];
    }
}

72.编辑距离

72. 编辑距离

给你两个单词 word1word2请返回将 word1 转换成 word2 所使用的最少操作数

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

  • 插入一个字符
  • 删除一个字符
  • 替换一个字符

思路:

dp[i][j] : dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]

代码:

class Solution {
    public int minDistance(String word1, String word2) {
        // 1. 建立dp数组
        int len1 = word1.length(), len2 = word2.length();
        int[][] dp = new int[len1 + 1][len2 + 1];

        // 2. 初始化
        for(int i = 0;i <= len1;i++) dp[i][0] = i;// 此时两字符相等其中一个必须全部删除
        for(int j = 0;j <= len2;j++) dp[0][j] = j;// 下标到i/j表示统计到i-1, j-1, 一共有i/j个字符

        // 3. 根据递推公式遍历
        for(int i = 1;i <= len1;i++) {// 0已经初始化
            for(int j = 1;j <= len2;j++) {// 0已经初始化
                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 - 1][j], dp[i][j - 1])) + 1;
                    // 1. dp[i - 1][j - 1]的基础上修改当前两个其中一个使其和另一个字符相等
                    // 2. dp[i - 1][j]的基础上再删除一个dp[i]
                    // 3. dp[i][j - 1]的基础上再删除一个dp[j]
                }
            }
        }

        // 4. 返回
        return dp[len1][len2];
    }
}

647.回文子串

647. 回文子串

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

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

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

思路:

  1. 确定dp数组(dp table)以及下标的含义布尔类型的dp[i][j]:表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]为true,否则为false。
  2. 确定递推公式s[i]!=s[j],dp[i] [j]=false。s[i]==s[j]
if (s[i] == s[j]) {
    if (j - i <= 1) { // 情况一 和 情况二
        result++;
        dp[i][j] = true;
    } else if (dp[i + 1][j - 1]) { // 情况三
        result++;
        dp[i][j] = true;
    }
}

result就是统计回文子串的数量。

- 情况一:i==j,同一个字符例如a,当然是回文子串
- 情况二:j - i ==1,例如aa,也是回文子串
- 情况三:j - i > 1,例如cabac,此时s[i]与s[j]已经相同了,我们看i到j区间是不是回文子串就看aba是不是回文就可以了,那么aba的区间就是 i+1 与 j-1区间,这个区间是不是回文就看dp[i + 1] [j - 1]是否为true。
  1. dp数组如何初始化dp[i] [j] = false,刚开始不可能全都匹配上。
  2. 确定遍历顺序首先从递推公式中可以看出,情况三是根据dp[i + 1] [j - 1]是否为true,在对dp[i] [j]进行赋值true的。dp[i + 1] [j - 1] 在 dp[i][j]的左下角所以一定要从下到上,从左到右遍历,这样保证dp[i + 1][j - 1]都是经过计算的
  3. 举例推导dp数组

代码:

class Solution {
    public int countSubstrings(String s) {
        // 1. 建立dp数组
        char[] str = s.toCharArray();
        int len = str.length;
        // dp[i][j]: s(i) ~ s(j) 组成的字符串是否为回文子串
        boolean[][] dp = new boolean[len][len];
        // 根据dp数组统计最终返回的结果
        int res = 0;

        // 2. 初始化: 全部默认初始化为false

        // 3. 根据递推公式进行遍历
        for(int i = len - 1;i >= 0;i--) {// 外层从下往上
            for(int j = i;j < len;j++) {// 内层从左往右
                // 默认初始值为false, 只有当前两字符相等时才有可能赋值为true
                if(str[i] == str[j]) {
                    if(j - i <= 1) {// a | aa
                        dp[i][j] = true;
                        res++;
                    }else if(dp[i + 1][j - 1]) {// a xxxxx a, 根据xxxxx进行判断
                        dp[i][j] = true;
                        res++;
                    }
                }
            }
        }

        // 4. 返回
        return res;
    }
}

516.最长回文子序列

516. 最长回文子序列

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

子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。

示例 1:

输入:s = "bbbab"
输出:4
解释:一个可能的最长回文子序列为 "bbbb" 。

示例 2:

输入:s = "cbbd"
输出:2
解释:一个可能的最长回文子序列为 "bb" 。

提示:

  • 1 <= s.length <= 1000
  • s 仅由小写英文字母组成

思路:

  1. 确定dp数组(dp table)以及下标的含义

     `dp[i][j]`**:字符串s在[i, j]范围内最长的回文子序列的长度为**`dp[i][j]`。
    
  2. 确定递推公式

    • 如果s[i]与s[j]相同,那么dp[i][j] = dp[i + 1][j - 1] + 2;
    • 如果s[i]与s[j]不相同,说明s[i]和s[j]的同时加入 并不能增加[i,j]区间回文子序列的长度,那么分别加入s[i]、s[j]看看哪一个可以组成最长的回文子序列。
      • 加入s[j]的回文子序列长度为dp[i + 1][j]
      • 加入s[i]的回文子序列长度为dp[i][j - 1]
    • 那么dp[i][j]一定是取最大的,即:dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
  3. dp数组如何初始化从递推公式:dp[i][j] = dp[i + 1][j - 1] + 2; 可以看出 递推公式是计算不到 i 和j相同时候的情况。所以需要手动初始化一下,当i与j相同,那么dp[i][j]一定是等于1的,即:一个字符的回文子序列长度就是1。其他情况dp[i][j]初始为0就行,这样递推公式:dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]); 中dp[i][j]才不会被初始值覆盖。

  4. 确定遍历顺序从递归公式中,可以看出,dp[i][j] 依赖于 dp[i + 1][j - 1] dp[i + 1][j]dp[i][j - 1]所以遍历i的时候一定要从下到上遍历,这样才能保证下一行的数据是经过计算的

  5. 举例推导dp数组

代码:

class Solution {
    public int longestPalindromeSubseq(String s) {
        // 1. 创建dp数组, dp[i][j]: s[i] ~ s[j]范围内的最长回文子序列
        int len = s.length();
        int[][] dp = new int[len][len];

        // 2. 初始化: 递推公式一直往中间集中, 初始化dp[i][i]只有一个字符, 长度为1的回文字符列
        for(int i = 0;i < len;i++) dp[i][i] = 1;

        // 3. 根据递推公式遍历
        for(int i = len - 1;i >= 0;i--) {// 从下往上
            for(int j = i + 1;j < len;j++) {// 从左往右
                if(s.charAt(i) == s.charAt(j)) {// 当前字符相等
                    dp[i][j] = dp[i + 1][j - 1] + 2;// i+1 ~ j-1 字符串的最长回文子序列 + 2
                }else {// 当前字符不等
                    dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);// 分别考虑两边的字符的最长回文子序列
                }
            }
        }

        // 4. 返回
        return dp[0][len - 1];
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2319739.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

星越L_陡坡缓降使用讲解

目录 1.陡坡缓降 1.陡坡缓降 中控屏下滑-点击陡坡缓降功能 35km/h以下时生效。35km/h-60km/h该功能暂停 60km/h以上该功能关闭

XSS跨站脚本攻击漏洞(Cross Site Scripting)

前提概要 本文章主要用于分享XSS跨站脚本攻击漏洞基础学习&#xff0c;以下是对XSS跨站脚本攻击漏洞的一些个人解析&#xff0c;请大家结合参考其他文章中的相关信息进行归纳和补充。 XSS跨站脚本攻击漏洞描述 跨站脚本攻击&#xff08;XSS&#xff09;漏洞是一种常见且危害较…

html5基于Canvas的经典打砖块游戏开发实践

基于Canvas的经典打砖块游戏开发实践 这里写目录标题 基于Canvas的经典打砖块游戏开发实践项目介绍技术栈核心功能实现1. 游戏初始化2. 游戏对象设计3. 碰撞检测系统4. 动画系统5. 用户界面设计 性能优化1. 渲染优化2. 内存管理 项目亮点技术难点突破项目总结 项目介绍 在这个…

企业信息化的“双螺旋”——IT治理和数据治理

企业信息化的“双螺旋”——IT治理和数据治理 一、核心定义二、关键差异三、内在联系四、实践挑战与融合路径五、行业案例参考六、结论数据治理(Data Governance)和IT治理(IT Governance)是现代企业数字化转型中的关键概念,二者既有紧密关联又各有侧重。以下从定义、核心内…

CCBCISCN复盘

AWDP – ccfrum 自己搭了一下环境, 复现一下这道题目, 之前比赛的时候完全没想到这个漏洞要怎么打, 修也不知道要怎么修, 就仅仅是对用户名的账号和密码进行了一下过滤, 完全没起到作用, 唉, 实在太菜 如果想要尝试复现的话可以尝试拉取这个镜像, 我打完之后就直接把这个容器给…

糊涂人寄信——递推

思路分析&#xff1a;当有n封信&#xff0c;n个信封时。第k封信没有装在第k个信封里&#xff08;k从1~n&#xff09;&#xff0c;就算所有的信封都装错了。我们可以得知的是&#xff0c;当有1封信,时&#xff0c;装错类别数为0。当有两封信时&#xff0c;装错类别为1。 当有三…

使用 OpenCV 拼接进行图像处理对比:以形态学操作为例

图像处理在计算机视觉中起着至关重要的作用&#xff0c;而 OpenCV 作为一个强大的图像处理库&#xff0c;提供了丰富的函数来实现各类图像处理任务。形态学操作&#xff08;Morphological Operations&#xff09;是其中常用的技术&#xff0c;尤其适用于二值图像的处理。常见的…

OpenHarmony 入门——ArkUI 跨页面数据同步和页面级UI状态存储LocalStorage小结(二)

文章大纲 引言一、在代码逻辑使用LocalStorage二、从UI内部使用LocalStorage三、LocalStorageProp和LocalStorage单向同步四、LocalStorageLink和LocalStorage双向同步五、兄弟组件之间同步状态变量七、将LocalStorage实例从UIAbility共享到一个或多个视图 引言 前面一篇文章主…

Python数据可视化实战:从基础图表到高级分析

Python数据可视化实战&#xff1a;从基础图表到高级分析 数据可视化是数据分析的重要环节&#xff0c;通过直观的图表可以快速洞察数据规律。本文将通过5个实际案例&#xff0c;手把手教你使用Python的Matplotlib库完成各类数据可视化任务&#xff0c;涵盖条形图、堆积面积图、…

在 Elasticsearch 中扩展后期交互模型 - 第 2 部分 - 8.18

作者&#xff1a;来自 Elastic Peter Straer 及 Benjamin Trent 本文探讨了如何优化后期交互向量&#xff0c;以适应大规模生产工作负载&#xff0c;例如减少磁盘空间占用和提高计算效率。 在之前关于 ColPali 的博客中&#xff0c;我们探讨了如何使用 Elasticsearch 创建视觉搜…

蓝桥每日打卡--区间移位

#蓝桥#JAVA#区间移位 题目描述 数轴上有n个闭区间&#xff1a;D1,⋯Dn。 其中区间Di用一对整数[ai,bi]来描述&#xff0c;满足 ai≤bi。 已知这些区间的长度之和至少有。 所以&#xff0c;通过适当的移动这些区间&#xff0c;你总可以使得他们的"并"覆盖 [0,],也…

CUDAOpenCV 基于Hessian矩阵计算特征值

文章目录 一、简介二、实现代码三、实现效果一、简介 基于之前的博客:CUDA&OpenCV Hessain矩阵计算,我们可以计算出每个像素的特征值: 二、实现代码 ComputeHessainMatrix.cuh #ifndef HESSAIN_GPU_CUH #

基于CAMEL 的Workforce 实现多智能体协同工作系统

文章目录 一、workforce 简介1.架构设计2.通信机制 二、workforce 工作流程图示例1.用户角色2.工作流程 三、workforce 中重要函数说明1.__init__函数2.add_single_agent_worker 函数3.add_role_playing_worker 函数4.add_workforce 函数 四、基于workforce实现多智能体协调&am…

PostgreSQL_数据表结构设计并创建

目录 前置&#xff1a; 1 数据表设计思路 2 数据表格SQL 3 创建 3.1 创建数据库 db_stock 3.2 在 pgAdmin4 中创建表 前置&#xff1a; 本博文是一个系列。在本人“数据库专栏”-》“PostgreSQL_”开头的博文 1 数据表设计思路 1 日数据来自优矿&#xff0c;优矿的数据…

如何在MCU工程中启用HardFault硬错误中断

文章目录 一、HardFault出现场景二、启动HardFault三、C代码示例 一、HardFault出现场景 HardFault&#xff08;硬故障&#xff09; 错误中断是 ARM Cortex-M 系列微控制器中一个较为严重的错误中断&#xff0c;一旦触发&#xff0c;表明系统遇到了无法由其他异常处理机制解决…

MySQL -- 复合查询

数据库的查询是数据库使用中比较重要的环节&#xff0c;前面的基础查询比较简单&#xff0c;不做介绍&#xff0c;可自行查阅。本文主要介绍复合查询&#xff0c;并结合用例进行讲解。 本文的用例依据Soctt模式的经典测试表&#xff0c;可以自行下载&#xff0c;也可以自己创建…

卷积神经网络 - 卷积层(具体例子)

为了更一步学习卷积神经网络之卷积层&#xff0c;本文我们来通过几个个例子来加深理解。 一、灰度图像和彩色图像的关于特征映射的例子 下面我们通过2个例子来形象说明卷积层中“特征映射”的概念&#xff0c;一个针对灰度图像&#xff0c;一个针对彩色图像。 例子 1&#x…

测试Claude3.7 sonnet画蛋白质

测试Claude3.7 sonnet画蛋白虽然画的很粗糙&#xff0c;但是大致画了出来

java项目之基于ssm的游戏攻略网站(源码+文档)

项目简介 游戏攻略网站实现了以下功能&#xff1a; 管理员主要负责填充图书和其类别信息&#xff0c;并对已填充的数据进行维护&#xff0c;包括修改与删除&#xff0c;管理员也需要审核老师注册信息&#xff0c;发布公告信息&#xff0c;管理自助租房信息等。 &#x1f495;…

本地基于Ollama部署的DeepSeek详细接口文档说明

前文&#xff0c;我们已经在本地基于Ollama部署好了DeepSeek大模型&#xff0c;并且已经告知过如何查看本地的API。为了避免网络安全问题&#xff0c;我们希望已经在本地调优的模型&#xff0c;能够嵌入到在本地的其他应用程序中&#xff0c;发挥本地DeepSeek的作用。因此需要知…