数据结构与算法学习——动态规划-3

简介: 数据结构与算法学习——动态规划-3

数据结构与算法学习——动态规划-3


目录


博主介绍


前言


1、礼物的最大价值

1.2、最长不含重复字符的子字符串

1.3、把数字翻译成字符串

1.4、删除并获得点数

1.5、接雨水

1.6、丑数 II

1.7、跳跃游戏 I

1.8、跳跃游戏 II

1.9、环形子数组的最大和

2、乘积最大子数组

2.1、乘积为正数的最长子数组长度

背包

1.1、背包回顾

1.2、使用一维滚动数组优化背包问题

1.3、目标和

1.4、使用一维滚动数组优化分割等和子集问题

1.5、最后一块石头的重量 II

1.6、一和零

完全背包问题

1.1、完全背包问题理论说明

1.2、零钱兑换

1.3、零钱兑换 II

1.4、组合总和 IV

1.5、爬楼梯

1.6、完全平方数

背包问题总结

1.1、根据所给条件分类

1.2、根据待求项分类

1.3、组合问题和排列问题

编辑距离问题总结

1.1、判断子序列

1.2、不同的子序列

1.3、两个字符串中的删除操作

1.4、编辑距离

💫点击直接资料领取💫


目录

博主介绍

💂 个人社区:CSDN全国各地程序猿


🤟作者介绍:中国DBA联盟(ACDU)成员,CSDN全国各地程序猿(媛)聚集地管理员。目前从事工业自动化软件开发工作。擅长C#、Java、机器视觉、底层算法等语言。2019年成立柒月软件工作室,2021年注册苏州凯捷智能科技有限公司


💬如果文章对你有帮助,欢迎关注、点赞、收藏(一键三连)和C#、Halcon、python+opencv、VUE、各大公司面试等一些订阅专栏哦


💅 有任何问题欢迎私信,看到会及时回复


👤 微信号:stbsl6,微信公众号:苏州程序大白


🎯 想加入技术交流群的可以加我好友,群里会分享学习资料


前言


4774a992b3564dd3869a831cdeece54c.png


🥝数据结构与算法学习——动态规划-1🥝


🥝数据结构与算法学习——动态规划-2🥝


继续数据结构与算法学习——动态规划-1、动态规划-2讲解


1、礼物的最大价值


1、题目描述


在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?


示例一


输入: 
[
  [1,3,1],
  [1,5,1],
  [4,2,1]
]
输出: 12
解释: 路径 1→3→5→2→1 可以拿到最多价值的礼物


2、解题思路


确定 dp 数组的含义

其中 dp[i][j] 表示到达棋盘上的 [i , j] 位置时,所能拿到的礼物的总价值。


状态转移方程

对于棋盘上的某个位置 (i, j) 来说 ,它的状态可以由两个状态得来,即它的上方元素 (i - 1,j) 和左边元素 (i, j - 1) 得到,同时还需要加上当前位置礼物的价值 nums[i][j],我们需要在上面的选择中取较大值。


dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]) + nums[i][j];


初始化 dp 数组

我们需要初始化第一行和第一列的元素


其中第一行的元素为 nums[0,i]上的元素累加值,比如说,在例子中, dp[0][0]应该初始化为 nums[0][0],dp[0][1]应该初始化为 nums[0][0] + nums[0][1],dp[0][2] = nums[0][0] + nums[0][1] + nums[0][2]。


同理,第一列的元素也应该按照上面的规则初始化,只不过变为向下累加。


遍历顺序

这道题是最常规的动态规划问题,我们只需要按照从头到尾的顺序进行遍历即可,可以看到 dp[i][j] 是由上一行、前一列的元素推导出来的。


3、解题代码


   public int maxValue(int[][] grid) {
       if (grid == null || grid.length == 0) return 0;
       int row = grid.length;
       int column = grid[0].length;
       int[][] dp = new int[row][column];
       int value = 0;
// 初始化 dp 数组
       for (int i = 0;i < row;i++) {
           value += grid[i][0];
           dp[i][0] = value;
       }
       value = 0;
       for (int j = 0;j < column;j++) {
           value += grid[0][j];
           dp[0][j] = value;
       }
       for (int i = 1;i < row;i++) {
           for (int j = 1;j < column;j++) {
               dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
           }
       }
       return dp[row - 1][column - 1];
   }

 

1.2、最长不含重复字符的子字符串


1、题目描述


请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。


示例一


输入: "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。


示例二


输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。


2、解题思路


明确 dp 数组的含义

其中 dp[i] 表示以字符 s.charAt(i) 结尾的最长不含重复字符的子字符串的长度。


明确状态转移方程

对于 i 位置的字符 x 来说,我们需要找到 x 在 s 中的上一个位置 index ,然后根据 index 来判断下一步该如何选择。


1、如果 index == -1 ,此时代表 x 字符串在 [0, i - 1]位置内不曾出现,那么 x 字符可以加入到以 i - 1 字符为结尾的最长不含重复字符的子字符串中,即此时有 dp[i] = dp[i - 1] + 1 ,比如说当前字符串为 abcde ,此时 e 为 x ,e 未曾在之前的字符串中出现,那么将其加入到以 d 结尾的子字符串中,不会打破原来的不重复规则。


2、如果找到了一个不为 -1 的 index ,那么证明 x 字符在 [0, i - 1] 位置内出现过,如果:


dp[i - 1] < i - index ,此时说明字符 s[index] 在子字符串 dp[i - 1]区间之外,此时将 s[i]加入到以 s[i - 1]为结尾的最长不重复子串中,不会破坏规则,此时 dp[i] = dp[i - 1] + 1,比如说对于字符串 axxbcda 中,s[i] = 'a',而前一个 ‘a’ 出现在dp[i - 1]的最长不重复子串之外,所以将 s[i] 加入到 s[i - 1]结尾的最长不重复子串中不会破坏规则。


dp[i - 1] >= i - index,说明字符串 s[index]在子字符串 dp[i - 1] 的区间之内,所以此时以 s[i] 为结尾的最长不重复子串的长度由 s[index]中 index 的位置决定,即dp[i] = i - index。


3、我们使用一个 Map 来保存字符 s[i] 的上一个出现位置,如果不存在,那么返回 -1 ,在循环过程中,需要更新这个 Map。


3、解题代码


public int lengthOfLongestSubstring(String s) {
    if (s == null || s.length() == 0) return 0;
    Map<Character, Integer> memo = new HashMap<>();
    // 由于 dp[i] 只与前一个状态 dp[i - 1] 有关,所以使用一个变量进行滚动
    int temp = 0, result = 0;
    for (int i = 0;i < s.length();i++) {
        // 如果没有对应的 key ,那么返回 -1
        int index = memo.getOrDefault(s.charAt(i), -1);
        // 更新 map 
        memo.put(s.charAt(i), i);
        temp = temp < i - index ? temp + 1 : i - index;
        result = Math.max(temp, result);
    }
    return result;
}


1.3、把数字翻译成字符串


1、题目描述


给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 “a” ,1 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。


示例一


输入: 12258
输出: 5
解释: 12258有5种不同的翻译,分别是"bccfi", "bwfi", "bczi", "mcfi"和"mzi"


2、解题思路


使用动态规划解决这道题。


明确 dp 数组的含义。


我们设dp[i] 的含义为:下标范围为 [0, i] 的那部分数字有 dp[i] 种翻译方法。


明确状态转移方程

对于dp[i] 而言,我们需要进行分类讨论。


1、当数字中的最后两个数字,即[i - 1, i] 范围的数字可以被翻译为一个数时,0 =< (i - 1) * 10 + i <= 25时,那么我们有两种选择,我们即可以直接将最后一个数字翻译为一个字母,此时存在等式 dp[i] = dp[i - 1] ;又可以将最后两个数字直接翻译成一个字母,此时存在等式 dp[i] = dp[i - 2]。


情况 1 需要综合两个选择进行考虑,所以这种情况下 dp[i] = dp[i - 1] + dp[i - 2]。



4bdaa2d8ab444e8d9c30ed2e98b93125.png


1、当数字中的最后两个数字无法直接被翻译为一个数时,此时我们别无选择,只有将最后一个数直接翻译成一个字母,此时存在等式dp[i] = dp[i - 1]。


49d154022a04451480d94084cbac3d79.png


故我们可以得到以下的状态转移方程。


if (最后两个数可以翻译为一个字母) {
    dp[i] = dp[i - 1] + dp[i - 2];
} else {
    dp[i] = dp[i - 1];
}


数组初始化,对于第一个数字,我们只有将它直接转换为一个字母,此时 dp[0] = 1。

3、解题代码


    public int translateNum(int num) {
        //1 先将 num 转换为字符串
        String str = String.valueOf(num);
        // 创建一个 dp 数组,长度为 str.length()
        int[] dp = new int[str.length()];
        // 初始化 dp 数组
        dp[0] = 1;
        // 从 1 开始
        for (int i = 1;i < str.length();i++) {
            // 如果前一个数等于 1 ,或者前一个数等于 2 且当前数小于等于 5 
            if (str.charAt(i - 1) == '1' || (str.charAt(i - 1) == '2' && str.charAt(i) <= '5')) {
                if (i == 1) {
                    // 如果 i == 1,那么直接初始化为 2 
                    dp[i] = 2;
                } else {
                    dp[i] = dp[i - 2] + dp[i - 1];
                }
            } else {
                dp[i] = dp[i - 1];
            }
        }
        return dp[str.length() - 1];
    }

 

1.4、删除并获得点数


1、题目描述


给你一个整数数组 nums ,你可以对它进行一些操作。


每次操作中,选择任意一个 nums[i] ,删除它并获得nums[i] 的点数。之后,你必须删除 所有 等于 nums[i] - 1和nums[i] + 1的元素。


开始你拥有 0 个点数。返回你能通过这些操作获得的最大点数。


示例一


输入:nums = [3,4,2]
输出:6
解释:
删除 4 获得 4 个点数,因此 3 也被删除。之后,删除 2 获得 2 个点数。总共获得 6 个点数。


示例二


输入:nums = [2,2,3,3,3,4]
输出:9
解释:
删除 3 获得 3 个点数,接着要删除两个 2 和 4 。之后,再次删除 3 获得 3 个点数,再次删除 3 获得 3 个点数。总共获得 9 个点数。


2、解题思路


这道题与打家劫舍的思路差不多,如果我们在原来数组的基础上构造一个新数组,这个数组以 元素的值为下标,nums[i] 表示 i 出现的次数,以示例二的数组为例,构造出来的新数组为:


nums = {2,2,3,3,3,4}
temp = {0,0,2,3,1}


代表原来的数组中有 0 个 0 ,0 个 1 ,2 个 2 ,3 个 3 ,一个 4。


这样就变成了打家劫舍问题。


dp 数组的含义:

其中 dp[i] 表示面对值为 i 的数时,能获得的最大点数为 dp[i]。


状态转移方程

和打家劫舍一样,我们在选择值为 i 的数时,不需要考虑 i + 1 的数,只需要考虑 i - 1 的情况,此时 dp[i] 有两个选择:


1、第一个选择就是选择获取 i - 1 的点数,此时 i 的点数已经不能选择(因为 i 的值),所以此时有式子为 dp[i] = dp[i - 1]。


2、第二个选择就是选择获取 i - 2的点数,此时可以选择 i 的点数,此时存在式子 dp[i] = dp[i - 2] + i * temp[i]。


所以状态转移方程如下:


dp[i] = Math.max(dp[i - 1], dp[i - 2] + i * temp[i]);


数组初始化

和打家劫舍的 dp 数组初始化一样,但注意这里需要对构造出来的新数组进行初始化。


3、解题代码


public int deleteAndEarn(int[] nums) {
    if (nums == null || nums.length == 0) return 0;
    //1 先找出数组中的最大数,用于构造新数组
    int max = Integer.MIN_VALUE;
    for (int num : nums) {
        max = Math.max(max, num);
    }
    if (max <= 0) return 0;
    // 必须是最大值 + 1 长度
    int[] temp = new int[max + 1];
    for (int num : nums) {
        temp[num]++;
    }
    // 接下来,就按照打家劫舍的方法做
    if (temp.length == 1) return temp[0];
    if (temp.length == 2) return Math.max(temp[0], temp[1]);
    int one = temp[0];
    int two = Math.max(temp[0], temp[1]);
    int cur = 0;
    for (int i = 2;i < temp.length;i++) {
        cur = Math.max(two, one + i * temp[i]);
        one = two;
        two = cur;
    }
    return cur;
}


1.5、接雨水


1、题目描述


给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。


2、解题思路


dp 数组的含义,其中 dp[i] 表示在下标为 i 的柱子上可以装的水。

我们设第 i 根柱子左边最高的柱子高度为 maxLeft, 同时记第 i 根柱子右边最高的柱子高度为 maxRight ,则第 i 根柱子上能承载的水量为:


dp[i] = Math.max(0, Math.min(maxLeft, maxRight) - height[i])


如果我们每次都需要向左右两边遍历找寻最大值,那么时间复杂度就会非常高。


创建两个长度为 n 的数组 leftMax 和 rightMax , 对于 0 <= i < n ,leftMax[i]表示下标 i 及其左边的位置中,height的最大高度;而 rightMax[i] 表示下标 i 及其右边的位置中,height 的最大高度。


1、leftMax[0] = height[0], rightMax[n - 1] = height[n - 1]。


2、当 1 <= i <= n - 1 时,leftMax[i] = Math.max(leftMax[i - 1], height[i])。


3、当 0 <= i <= n - 2 时,rightMax[i] = Math.max(rightMax[i - 1], height[i])。


因此可以正向遍历数组 height 得到数组 leftMax 的每个元素值,反向遍历数组 height 得到数组 rightMax 的每个元素值。


这道题中,我们不需要使用到 dp 数组,只需要使用一个变量进行保存同时累加即可。


状态转移方程

我们设在第 i 根柱子上能存的水为 count ,则:


int count = Math.max(0, Math.min(leftMax[i], rightMax[i]) - height[i])


3、解题代码


public int trap(int[] height) {
    if (height == null || height.length <= 2) return 0;
    int[] leftMax = new int[height.length];
    leftMax[0] = height[0];
    // 正向遍历,获取每根柱子左边的最大高度
    for (int i = 1;i < height.length;i++) {
        leftMax[i] = Math.max(leftMax[i - 1], height[i]);
    }
    int[] rightMax = new int[height.length];
    // 反向遍历,获取每根柱子右边的最大高度
    rightMax[height.length - 1] = height[height.length - 1];
    for (int i = height.length - 2;i >= 0;i--) {
        rightMax[i] = Math.max(rightMax[i + 1], height[i]);
    }
    // 使用一个变量来计算总水量
    int sum = 0;
    // 第一根和最后一根柱子没有结果,直接跳过
    for (int i = 1;i < height.length - 1;i++) {
        int count = Math.max(0, Math.min(leftMax[i], rightMax[i]) - height[i]);
        sum += count;
    }
    return sum;
}


1.6、丑数 II


1、题目描述


给你一个整数 n ,请你找出并返回第 n 个 丑数 。丑数 就是只包含质因数 2、3 和/或 5 的正整数。(1 也可以看为一个丑数)。


示例一


输入:n = 10
输出:12
解释:[1, 2, 3, 4, 5, 6, 8, 9, 10, 12] 是由前 10 个丑数组成的序列。=


丑数计算思路


由于 1 也是丑数,那么我们可以将 1 作为基础乘以 2,3,5 ,得到的结果也是丑数,再将这些得到的结果分别乘以 2,3,5 ,得到的结果也都是丑数,然后按照从小到大取到第 n 个结果,就是第 n 个丑数:


32feeae7e65a43c9a23005a76986de92.png


2、解题思路


使用最小堆

在这个解决思路中,我们需要创建一个小顶堆和一个集合,其中集合用于取出重复的丑数,比如说上图中生成的丑数存在相同的情况,我们需要进行去重。


初始化时,最小堆中只存在丑数 1。


循环遍历,将 1 从最小堆中取出,然后将 1 乘以三个质因子,然后将结果放入最小堆中(需要先到集合中看一下有没有这个元素,如果有那么就不加入到最小堆中)。


不断从堆中取出元素,然后乘以质因子,再将没出现过的结果放入到最小堆中,循环 n 次,拿到的就是第 n 个丑数。


动态规划

1、dp 数组的含义:


其中 dp[i] 表示第 i 个丑数的值,则第 n 个丑数即为 dp[n]。


2、状态转移方程:


定义三个指针 p2,p3,p5 ,表示下一个丑数是当前指针指向的丑数乘以对应的质因子。初始化时,三个指针的值均为 1 ,当 2 <= i <= n 时,有:


dp[i] = Math.min(Math.min(dp[p2] * 2, dp[p3] * 3),dp[p5] * 5);


然后我们需要比较 dp[i] 与 dp[p2] * 2 ,dp[p3] * 3 ,dp[p5] * 5 是否相等,如果相等则将对应的指针 + 1。


从上面的图中,当求第二个丑数时,结果从 {2,3,5} 中 诞生,我们得到第二个丑数为 2 ,求第三个丑数时,我们在{3,5,4} 中求结果,可以看到 {3,5} 是上次求丑数留下来的结果,而 4 正是 2 * 2 ,即 1 后移( + 1)后 * 2 的结果:


3、dp 数组初始化:


dp[1] 表示第一个丑数,初始化为 1。


3、解题代码


最小堆

这里为了防止溢出,使用的是 Long 型。


public int nthUglyNumber(int n) {
    if (n == 1) return 1;
    // 创建一个最小堆,用于存放丑数
    PriorityQueue<Long> minHeap = new PriorityQueue<>();
    // 创建一个数组,用于存放质因子。
    int[] nums = {2, 3, 5};
    Set<Long> set = new HashSet<>();
    // 初始化,我们需要将 1 放入最小堆中
    minHeap.add(1L);
    long result = 0;
    for (int i = 0;i < n;i++) {
        result = minHeap.poll();
        for (int num : nums) {
            long uglyNum = num * result;
            // 只有集合中不存在时,才将结果加入到最小堆中
            if (!set.contains(uglyNum)) {
                minHeap.add(uglyNum);
                set.add(uglyNum);
            }
        }
    }
    return (int)result;
}


动态规划


public int nthUglyNumber(int n) {
    if (n == 1) return 1;
    int[] dp = new int[n + 1];
    dp[1] = 1;
    int p2 = 1, p3 = 1, p5 = 1;
    for (int i = 2;i <= n;i++) {
        dp[i] = Math.min(Math.min(dp[p2] * 2, dp[p3] * 3), dp[p5] * 5);
        if (dp[i] == dp[p2] * 2) {
            p2++;
        }
        if (dp[i] == dp[p3] * 3) {
            p3++;
        }
        if (dp[i] == dp[p5] * 5) {
            p5++;
        }
    }
    return dp[n];
}


1.7、跳跃游戏 I


1、题目描述


给定一个非负整数数组 nums ,你最初位于数组的 第一个下标 。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标。


2、解题思路


dp 数组的含义:

其中 dp[i] 表示能否到达下标为 i 的位置,所以 dp 应该是一个 boolean 类型的数组。


状态转移方程。

遍历 [0, i - 1] ,我们需要找到一个位置 j ,如果小人能到达 j 位置且满足条件 j + nums[j] >= i,那么证明 i 是可以到达的,此时我们可以使 dp[i] = true ,然后我们需要跳出循环。


if(dp[j] && j + nums[j] > i) {
  dp[i] = true;
}


dp 数组的初始化:

我们需要初始化 dp[0] = true,因为小人从 0 位置开始,同时需要返回 dp[nums.length - 1]。


3、解题代码


public boolean canJump(int[] nums) {
    if (nums == null || nums.length <= 1) return true;
    boolean[] dp = new boolean[nums.length];
    dp[0] = true;
    for (int i = 1;i < nums.length;i++) {
        for (int j = 0;j < i;j++) {
            if (dp[j] && nums[j] + j >= i) {
                dp[i] = true;
                break;
            }
        }
    }
    return dp[nums.length - 1];
}


1.8、跳跃游戏 II


1、题目描述


1、题目描述


1、题目描述
给定一个非负整数数组,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
你的目标是使用最少的跳跃次数到达数组的最后一个位置。


示例一


输入: [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。


2、解题思路


dp 数组的含义

其中 dp[i] 表示到达 i 位置所用的最小步数,我们需要返回 dp[nums.length - 1]。


状态转移方程

当我们处于第 i 个位置时,我们遍历 [0. i - 1] 位置,然后寻找在哪个位置可以直接用一步到达 i 位置。


if (j + nums[j] >= i) {
    // 如果在 j 位置可以直接到达 i 位置,那么直接在到达 j 所需要的最少步数 dp[j] 的基础上 + 1即可,但我们需要和 dp[i] 做比较,取最小值
    dp[i] = Math.min(dp[i], dp[j] + 1);
}=


初始化 dp 数组


由于在状体转移方程中,我们需要使用 min 函数来选择最小值,那么我们给数组的初始化值就不能是简单的 0 ,为了方便,我们先将数组中的所有元素全部置为 Integer,MAX_VALUE。


当 i 为 0 时,我们只需要 0 步就可以到达,所以 dp[0] = 0。


当 i 为 1 时,我们只需要一步就可以到达,所以 dp[1] = 1。


3、解题代码


public int jump(int[] nums) {
    int len = nums.length;
    if (len <= 1) return 0;
    // 创建一个大小为 len 的数组,并对它的值进行初始化
    int[] dp = new int[len];
    // 初始化数组的值为 Integer.MAX_VALUE ,因为我们需要与 0 作比较取最小值,所以要初始化一个值
    Arrays.fill(dp, Integer.MAX_VALUE);
    // 初始化 dp[0] 和 dp[1] ,当 i 等于 0 时,至少花 0 步就可以到达,当 i 等于 1 时,只需要一步就可以到达 1 
    dp[0] = 0;
    dp[1] = 1;
    for (int i = 2;i < len;i++) {
        for (int j = 0;j < i;j++) {
            // 如果在 [0, i - 1] 范围内有位置可以一步到达 i ,那么我们需要进行选择,
            if (j + nums[j] >= i) {
                dp[i] = Math.min(dp[i], dp[j] + 1);
            }
        }
    }
    return dp[len - 1];
}


1.9、环形子数组的最大和


1、题目描述


给定一个由整数数组 A 表示的**环形数组 C**,求 C 的非空子数组的最大可能和。
在此处,环形数组意味着数组的末端将会与开头相连呈环状。
(形式上,当0 <= i < A.length 时 C[i] = A[i],且当 i >= 0 时 C[i+A.length] = C[i])
此外,子数组最多只能包含固定缓冲区 A 中的每个元素一次。
(形式上,对于子数组 C[i], C[i+1], …, C[j],不存在 i <= k1, k2 <= j 其中 k1 % A.length = k2 % A.length)


示例一


输入:[1,-2,3,-2]
输出:3
解释:从子数组 [3] 得到最大和 3


示例二


输入:[5,-3,5]
输出:10
解释:从子数组 [5,5] 得到最大和 5 + 5 = 10


示例三


输入:[3,-1,2,-1]
输出:4
解释:从子数组 [2,-1,3] 得到最大和 2 + (-1) + 3 = 4


2、解题思路


1、对于一个环形数组来说,它的最大连续子数组其实只有两种情况:


第一种,最大连续子数组分布在数组中间,比如说下面的情况:

对于这种情况,我们只需要按照求普通数组的最大连续子数组的和的思路做即可。


4b186388f6944cd4a467c53a2e579350.png


第二种,最大连续子数组首尾相连,比如说下面的情况:

这种情况下,我们要求的值 answer 满足 answer = totalSum - min_so_far,其中 min_so_far表示图中灰色部分的和,即普通数组中最小连续子数组的和,我们可以借由最大连续子数组的和来做。


c9acf7ed57a6485d925d6200f7cbe11e.png


3、解题代码


public int maxSubarraySumCircular(int[] nums) {
    if (nums == null || nums.length == 0) return 0;
    int first = maxSubarray(nums);
    int total = 0;
    // 计算 total 的值
    for (int num : nums) {
        total += num;
    }
    int min = minSubarray(nums);
    int second = total - min;
    // 如果 total 等于 min ,那么证明差为 0 ,那么此时如果 first 返回的值是负数,那么就会返回错误的 0 
    if (total == min) return first;
    return Math.max(first, second);
}
private int minSubarray(int[] nums) {
    if (nums == null || nums.length == 0) return 0;
    if (nums.length == 1) return nums[0];
    int len = nums.length;
    int[] dp = new int[len];
    dp[0] = nums[0];
    int min = Integer.MAX_VALUE;
    for (int i = 1;i < len;i++) {
        dp[i] = Math.min(dp[i - 1] + nums[i], nums[i]);
        min = Math.min(dp[i], min);
    }
    return min;
}
private int maxSubarray(int[] nums) {
    if (nums == null || nums.length == 0) return 0;
    if (nums.length == 1) return nums[0];
    int len = nums.length;
    int[] dp = new int[len];
    int max = Integer.MIN_VALUE;
    dp[0] = nums[0];
    for (int i = 1;i < len;i++) {
        dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]);
        max = Math.max(dp[i], max);
    }
    return max;
}


2、乘积最大子数组


1、题目描述


给你一个整数数组 nums ,请你找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),


给你一个整数数组 nums ,请你找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),
并返回该子数组所对应的乘积。


示例一


输入: [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6。


示例二


输入: [-2,0,-1]
输出: 0
解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。


2、解题思路


与和最大子数组不一样的是,在本题中,我们需要根据当前遍历的元素来进行更新 dp 数组的值,我们设置两个变量,其中preMax 为 nums[0: i - 1]中最大的乘积,而preMin 为 nums[0: i - 1]中最小的乘积(可能为负数)。


当 nums[i]为正数时,此时由于正正得正,我们需要更新 preMax = Math.max(preMax * nums[i], nums[i]),同时更新 preMin = Math.min(preMax * nums[i], nums[i])。


当 nums[i]为负数时,此时由于负负为正得正,我们需要更新 preMax = Math.max(preMin * nums[i], nums[i]),同时更新 preMin = Math.min(preMin * nums[i], nums[i])。


我们使用一个 max 变量来记录全局最大值,将其与 preMax 比较。


3、解题代码


public int maxProduct(int[] nums) {
    if (nums == null || nums.length == 0) return 0;
    if (nums.length == 1) return nums[0];
    int preMax = nums[0], preMin = nums[0], max = nums[0];
    int curMax = 0, curMin = 0;
    for (int i = 1;i < nums.length;i++) {
        if (nums[i] > 0) {
            curMax = preMax * nums[i];
            curMin = preMin * nums[i];
        } else {
            curMax = preMin * nums[i];
            curMin = preMax * nums[i];
        }
        preMax = Math.max(curMax, nums[i]);
        preMin = Math.min(curMin, nums[i]);
        max = Math.max(preMax, max);
    }
    return max;
}


2.1、乘积为正数的最长子数组长度


1、题目描述


给你一个整数数组 nums ,请你求出乘积为正数的最长子数组的长度。
一个数组的子数组是由原数组中零个或者更多个连续数字组成的数组,请你返回乘积为正数的最长子数组长度。


示例一


输入:nums = [1,-2,-3,4]
输出:4
解释:数组本身乘积就是正数,值为 24 。


示例二


输入:nums = [-1,-2,-3,0,1]
输出:2
解释:乘积为正数的最长子数组是 [-1,-2] 或者 [-2,-3] 。


2、解题思路


与上一题相同,我们在遍历到某个元素时,根据这个元素的情况进行判断。


确定 dp 数组的含义。


我们需要创建两个数组,其中:


1、pos[i] 表示以 i 结尾的乘积为正数的最长子数组长度。


2、neg[i] 表示以 i 结尾的乘积为负数的最长子数组长度。


状态转移方程

1、当 nums[i] > 0 时,此时 nums[i] 的值不会影响之前子数组乘积的值的正负:


所以,如果 pos[i - 1] 大于 0 ,那么此时它可以放心乘上 nums[i] ,如果 pos[i - 1] 等于 0 ,
那么子数组 {nums[i]} 就是以 i 结尾的乘积为正数的最长子数组,即此时 pos[i] = 1,
所以我们可以直接列出当 nums[i] 大于 0 的状态转移方程;
而如果 neg[i - 1] 大于 0 ,那么它也可以放心乘上 nums[i],如果 neg[i - 1] 等于 0,
那么 neg[i ] 依然为 0。


if (nums[i] > 0) {
    pos[i] = pos[i - 1] + 1;
    neg[i] = neg[i - 1] > 0 ? neg[i - 1] + 1 : 0;
}


2、当 nums[i] < 0 时,此时 nums[i] 的值会影响之前子数组乘积的值的正负:


如果 pos[i - 1] 大于 0,那么此时我们将 nums[i] 放入以 i - 1 结尾的子数组中,
整个子数组的乘积变为了负数,所以此时有 neg[i] = pos[i - 1] + 1;如果 pos[i - 1] 等于 0 ,
那么此时如果我们将 nums[i] 放入以 i - 1 结尾的子数组中,可能会导致整个子数组乘积变为正,
所以此时我们直接记 neg[i] 为 1 。
如果 neg[i - 1] 大于 0,那么此时将 nums[i] 放入以 i - 1 结尾的子数组中,
整个子数组的乘积变为了正数,所以此时 pos[i] 的值应该更新为 neg[i - 1] + 1 ,
如果 neg[i - 1] 的值等于 0 ,那么应该将 pos[i] 置为 0。


if (nums[i] < 0) {
    neg[i] = pos[i - 1] + 1;
    pos[i] = neg[i - 1] > 0 ? neg[i - 1] + 1 : 0;
}


所以,我们可以得出状态转移方程如下:


if (nums[i] > 0) {
    pos[i] = pos[i - 1] + 1;
    neg[i] = neg[i - 1] > 0 ? neg[i - 1] + 1 : 0;
} else if (nums[i] == 0) {
    pos[i] = neg[i] = 0;
} else {
    neg[i] = pos[i - 1] + 1;
    pos[i] = neg[i - 1] > 0 ? neg[i - 1] + 1 : 0;
}


dp 数组的初始化。


这个需要根据 nums[0] 的值进行判断。


1、当 nums[0] 大于 0 时, pos[0] = 1,neg[0] = 0。


2、当 nums[0] 等于 0 时,直接初始化为 0。


3、当 nums[0] 小于 0 时,neg[0] = 1,pos[i] = 0。


3、解题代码


public int getMaxLen(int[] nums) {
    if (nums == null || nums.length == 0) return 0;
    if (nums.length == 1) return nums[0] > 0 ? 1 : 0;
    int[] pos = new int[nums.length];
    int[] neg = new int[nums.length];
    int max = Integer.MIN_VALUE;
    if (nums[0] > 0) {
        pos[0] = 1;
    } else if (nums[0] < 0) {
        neg[0] = 1;
    }
    for (int i = 1;i < nums.length;i++) {
        if (nums[i] > 0) {
            pos[i] = pos[i - 1] + 1;
            neg[i] = neg[i - 1] > 0 ? neg[i - 1] + 1 : 0;
        } else if (nums[i] == 0) {
            pos[i] = 0;
            neg[i] = 0;
        } else {
            neg[i] = pos[i - 1] + 1;
            pos[i] = neg[i - 1] > 0 ? neg[i - 1] + 1 : 0;
        }
        max = Math.max(max, pos[i]);
    }
    return max;
}


由于在状态转移方程中, dp[i] 的值只与前一个状态相关,所以我们可以使用一个变量进行滚动更新:


public int getMaxLen(int[] nums) {
    if (nums == null || nums.length == 0) return 0;
    if (nums.length == 1) return nums[0] > 0 ? 1 : 0;
    int posMax = 0;
    int negMax = 0;
    int max = Integer.MIN_VALUE;
    if (nums[0] > 0) {
        posMax = 1;
    } else if (nums[0] < 0) {
        negMax = 1;
    }
    int curPos = 0;
    int curNeg = 0;
    for (int i = 1;i < nums.length;i++) {
        if (nums[i] > 0) {
            curPos = posMax + 1;
            curNeg = negMax > 0 ? negMax + 1 : 0;
        } else if (nums[i] == 0) {
            curPos = 0;
            curNeg = 0;
        } else {
            curNeg = posMax + 1;
            curPos = negMax > 0 ? negMax + 1 : 0;
        }
        // 进行滚动
        negMax = curNeg;
        posMax = curPos;
        max = Math.max(max, curPos);
    }
    return max;
}


背包


1.1、背包回顾


1、题目描述


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

3933dd6c8016491da33aed94809f689f.png



2、使用动态规划求解


明确选择和状态

在这个问题中,状态包含两个变量,即背包的重量及所得到的价值,而选择也非常简单,就是是否选择将某件物品放入背包中。


明确 dp 数组的含义,这里使用一个二维的 dp 数组来解决这个问题,其中 dp[i][j] 表示从下标 [0, i] 的物品中任意取,放入容量为 j 的背包中,所能获得价值的最大总和为多少


明确递推公式


1、对于第 i 件物品,当剩余背包容量无法容纳该物品,或者我们选择不将其放入到背包中时,此时有关系如下:


dp[i][j] = dp[i - 1][j]


2、对于第 i 件物品,当剩余背包容量可以容纳该物品,且我们选择将其放入到背包时,此时存在关系如下,其中 weight[i] 、 value[i ] 分别指 i 物品的重量与价值。


dp[i][j] = dp[i - i][j - weight[i]] + value[i];


因此我们可以得到以下的递推公式:


if (j < weight[i]) {
  dp[i][j] = dp[i - 1][j]
} else {
  dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])
}


初始化 dp 数组


1、当 j 为 0 时,此时背包容量为 0 ,因此可获取的最大价值也为 0 ,即 dp[i][0] = 0。


2、当 i 为 0 时,即只有第一件物品可供选择时,如果此时背包容量 j >= weight[0] ,那么应该初始化对应的 dp[0][j] 为 value[0]。


1.2、使用一维滚动数组优化背包问题


1、优化思路


当使用二维数组时,背包问题的递推公式为 dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])。


可以看到,如果我们将 dp[i - 1] 那一层都拷贝到 dp[i] 上,那么表达式完全可以是 dp[i][j] = Math.max(dp[i][j], dp[i][j - weight[i]] + value[i])。


当前层的状态完全由上一层状态推导而来,对于这种情况,我们可以考虑使用滚动数组进行优化,这就是滚动数组的由来,需要满足的条件是上一层可以重复利用,直接拷贝到当前层。


2、含义介绍


确定 dp 数组的含义

在一维 dp 数组中, dp[j] 表示:容量为 j 的背包,所背的物品价值可以最大为 dp[j]。


确定递推公式

1、当我们选择不放物品 i 时,所得到的最大价值为 dp[j],这里可以类比我们使用二维数组的时候,如果选择不放物品 i 时,我们得到的最大价值为 dp[i - 1][j] , 等于是把 dp[i][j] 的上一层数据拷贝下来。


2、当我们选择放物品 i 时,所得到的最大价值为 dp[j - weight[i]] + value[i]。


所以,我们可以得到使用一维数组时,01 背包问题的递推公式为:


if (j < weight[i]) {
  dp[j] = dp[j];
} else {
  dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
}


dp 数组初始化


当 j 为 0 时,此时背包容量为 0 ,所以所能装的物品的最大价值也是 0 ,即 dp[0] = 0;
对于其他位置的值,我们同样将其初始化为 0 ,
避免因初始化值过大导致递推公式计算出来的值无法将初始值覆盖(将整个数组都初始化为 0 。


遍历顺序


在二维数组中,无论是先遍历物品,还是先遍历背包都是可以的,
但是在一维 dp 数组中,我们需要先遍历物品,然后再遍历背包,
同时需要注意的是,我们在遍历背包时,需要进行倒序遍历。


为什么要倒序遍历背包?


这是为了保证物品只被添加一次,在使用二维数组时,
当前层和上一层的数据是完全隔离开的,所以正序遍历和倒序遍历都可以,
但当我们使用一维数组时, 如果先计算 dp[j - 1] ,那么它可能会影响到后面 dp [j] 的数据,
我们可以来举一个例子,其中 weight = {1,3,4} ,values = {15,20,30} ,同时最大容量 capacity = 4。


1、当我们使用正序遍历时, dp[1] = Math.max(dp[1], dp[1 - weight[0]] + values[0]) = Math.max(dp[1], dp[1 - 1] + 15) = 15; dp[2] = Math.max(dp[2], dp[2 - weight[0]] + values[0]) = Math.max(dp[2], dp[2 - 1] + 15) = 30。


可以看到,如果我们使用正序遍历,那么在计算 dp[2] 时,递推公式中的 dp[2 - 1] = dp[1]是我们在上一次计算出来的 15 ,而且得到的结果也是 30 ,这相当于我们将第 0 件物品重复加入到了背包中。


1、当我们使用倒序遍历时, dp[2] = max(dp[2], dp[2 - weight[0]] + values[0]) = max (0, dp[1] + 15) = 15;dp[1] = max(dp[1], dp[1 - weight[0]] + values[0]) = max(0, dp[0] + 15) = 15。


此时可以看到,第 0 件物品只被加入到了一次,这是因为此时在计算 dp[j] 时, dp[j - 1]的结果还没有被计算出来,所以不会影响 dp[j]的计算。


3、解题代码


public static int knapsack(int number, int capacity, int[] weight, int[] values) {
    if (weight == null || values == null || weight.length != values.length) return 0;
    // 创建一个一维的 dp 数组,dp[j] 表示,当背包容量为 j 时,该背包能容纳物品的最大价值
    int[] dp = new int[capacity + 1];
    // 外层循环,先遍历物品
    for (int i = 0;i < number;i++) {
        // 内层循环,需要倒序遍历背包
        for (int j = capacity;j >= weight[i];j--) {
            // 使用递推公式,从右至左计算 dp[j] 的值
            dp[j] = Math.max(dp[j], dp[j - weight[i]] + values[i]);
        }
    }
    // 最后返回 dp[capacity] ,就是 capacity 容量的背包所能容纳的物品的最大价值
    return dp[capacity];
}


1.3、目标和


1、题目描述


给你一个整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 :


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

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


示例一


输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 3 。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3


示例二


输入:nums = [1], target = 1
输出:1


2、解题思路


使用动态规划来解决这道题,这道题也可以转换为 01 背包问题,与 01 背包问题不同的是,前者可以选择是否将这件物品装入背包中,而本题要求全部数字都被选中,但可以选择+ 和-。


将这个数组分割为两部分,其中被赋予 + 的那一部分称为 P ,而被赋予 - 的那一部分称为N,所以我们可以得到以下结论。


1、sum(P) - sum(N) = S。


2、sum(P) + sum(N)= 数组中所有元素的和 sum。


3、将上面的式子相加,有 2 * sum(P) = target + sum,移项有 sum(P) = (target + sum) / 2,我们记这个值为 result,即 result = (target + sum) / 2。


所以问题就转换为求容量为 result 的 01 背包问题,要装满容量为 result 的背包,有几种方案:


如果 target 的值大于 sum ,那么直接返回 0 ,因为此时无法找到一种可能的实现。


如果 sum§ 不是正数,即 (target + sum) 不是偶数,那么返回 0。


确定 dp 数组的含义


dp[j] 表示在数据 nums 中,凑出 j 的方法有 dp[j] 种,换成背包问题就是,如果要填充 j 这么大容积的包,有 dp[j] 种方法。


确定递推公式

1、填满容量为 j - nums[i] 的背包,有 dp[j - nums[i]] 种方法。


2、那么只需要获得 nums[i] ,我们就可以凑出 dp[j] ,此时得到 dp[j] 的方法有 dp[j - nums[i]] ,这里有点类似凑硬币。


比如说,如果我们当前需要计算 dp[5] ,同时我们手中有一个重量为2的物品,此时我们只需要计算出 dp[3] 就可以得到在我们有 重量为 2的物品的前提下,能填满容量为5的背包的方法,所以我们可以得到以下的递推公式。


dp[j] += dp[j - nums[i]]

1

初始化 dp 数组

这道题中,由于我们要求 dp[result] ,所以我们需要初始化一个长度为result + 1 的数组,同时,当 j == 0 时,使用 nums 数组中的元素可以有一种方法凑出 0 ,故 dp[0] = 1 ;


3、解题代码


这里使用一维滚动数组来解这道题。


public int findTargetSumWays(int[] nums, int target) {
    if (nums == null || nums.length == 0) return 0;
    //1 计算数组元素和
    int sum = gengerateSumByArray(nums);
    //2 如果 sum 小于 target 或者 (sum + target) 不是偶数,那么直接返回 0 
    if (sum < target || (sum + target) % 2 == 1) return 0;
    //3 使用一个变量保存 (sum + target) / 2;
    target = (sum + target) / 2;
    //这里需要进行判断,如果计算出来的 target 小于 0 ,那么需要将其变为相反数
    target = Math.abs(target);
    //4 创建一个长度为 target + 1 的数组,并进行初始化
    int[] dp = new int[target + 1];
    // 当 j == 0 时,此时在 nums 中,只能找出一种方案凑出 0 ,所以 dp[0] = 1
    dp[0] = 1;
    //5 外层循环遍历物品
    for (int num : nums) {
        //6 内层循环倒序遍历背包
        for (int j = target;j >= num;j--) {
            // 使用递推公式累加 dp[j] 的值
            dp[j] += dp[j - num];
        }
    }
    return dp[target];
}
private int gengerateSumByArray(int[] nums) {
    int sum = 0;
    for (int num: nums) sum += num;
    return sum;
}


1.4、使用一维滚动数组优化分割等和子集问题


1、优化思路


使用一维滚动数组代替原来的二维数组


dp 数组的含义


dp 是一个一维的 boolean 数组,其中 dp[j] 表示在数组中,是否可以找到一个子集,使得这些子集的和等于 j。


递推公式

当我们知道数组元素 nums[i] 时,此时如果 dp[j] 或 dp[j - nums[i]] 有一个为 true ,那么就代表 dp[j] 为 true ,即:


dp[j] = dp[j] || dp[j - nums[i]]


2、解题代码


/**
 * 这道题可以看为,在 nums 中,是否可以找到几个数,这几个数的和为 sum / 2 ,其中 sum 为数组元素和
 */
public boolean canPartition(int[] nums) {
    if (nums == null || nums.length == 0) return true;
    //1 计算数组元素和
    int sum = gengerateSumByArray(nums);
    // 如果数组元素和不是一个偶数,那么直接返回 false
    if (sum % 2 != 0) return false;
    int target = sum / 2;
    //2 我们需要在数组 nums 中,找到是否存在几个数字,满足这几个数字的和为 target
    // dp[j] 表示在数组 nums 中,是否存在几个数字,满足这几个数字的和为 j
    boolean[] dp = new boolean[target + 1];
    // 初始化 dp 数组,当 j == 0 时,此时可以找到几个数字,满足这几个数字的和为 0 
    dp[0] = true;
    for (int num : nums) {
        for (int j = target;j >= num;j--) {
            // 如果 dp[j] 或 dp[j - num] 有一个为真,那么 dp[j] 即为真,此时代表存在...
            dp[j] = dp[j] || dp[j - num];
        }
    }
    return dp[target];
}
private int gengerateSumByArray(int[] nums) {
    int sum = 0;
    for (int num : nums) sum += num;
    return sum;
}


1.5、最后一块石头的重量 II


1、解题思路


在上面有提到,这道题其实就是尽量将石头分成重量相同的两堆,然后让相撞之后剩下的石头最小。
这道题其实就是求,给你一个大小为 sum / 2 的背包,其中 sum 为 nums 的元素和,
然后尽可能地往背包中装东西,我们假设最多往背包中装了 maxWeight 重量的石头,
那么我们要求的结果就是 sum - 2 * maxWeight;


确定 dp 数组


使用一个一维的 boolean 类型的 dp 数组,
其中 dp[j] 的含义、状态转移方程与 分割等和子集 问题中 dp 数组的含义相同,即在数组 nums 中,
能否找到一个子集,使得这个子集的元素和的值等于 j 。


当计算好 dp 数组后,我们需要反向遍历 dp 数组,寻找第一个为 true 的元素值,然后得到结果并返回,结果为 sum - 2 * j。

2、解题代码


public int lastStoneWeightII(int[] stones) {
    if (stones == null || stones.length == 0) return 0;
    int sum = generateSumByArray(stones);
    // 计算出 target 的值,我们要尽可能装满这个容量为 target 的包
    int target = sum / 2;
    boolean[] dp = new boolean[target + 1];
    dp[0] = true;
    // 外层循环遍历物品
    for (int stone : stones) {
        // 内层循环反向遍历背包
        for (int j = target;j >= stone;j--) {
            dp[j] = dp[j] || dp[j - stone];
        }
    }
    // 填完表后,我们需要找到最接近 target 的一个 j 值
    for (int j = target;;j--) {
        // 找到第一个为 true 的j
        if (dp[j]) {
            return sum - 2 * j;
        }
    }
}
private int generateSumByArray(int[] stones) {
    int sum = 0;
    for (int stone : stones) sum += stone;
    return sum;
}


1.6、一和零


1、题目描述


给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。
如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。


示例 一


输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
输出:4
解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。
其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。


示例 二


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


2、解题思路


这道题也可以看为 01 背包问题,其中字符串数组中的每一个字符串都是一件物品,而 m 和 n 相当于一个二维的背包。


确定 dp 数组的含义,这里使用滚动数组进行优化,其中 dp[i][j]表示最多有 i 个 0 和 j 个 1 的 strs 的最大子集的大小为 dp[i][j]。


对于 dp[i][j]而言,它可以由前一个 strs 里的字符串推导出来,假设该字符串中含有 zeroNum 个 0 ,同时含有 oneNum个 1 ,那么有以下的递推公式。


dp[i][j] = Math.max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1)


如何初始化 dp 数组?

只需要将 dp 初始化为 0 即可:


3、解题代码


public int findMaxForm(String[] strs, int m, int n) {
    if (strs == null || strs.length == 0) return 0;
    // 创建一个二维数组,其中 dp[i][j] 表示子集最多能有 i 个 0 ,j 个 1 时,最大子集的长度
    int[][] dp = new int[m + 1][n + 1];
    // 外层循环遍历物品,在这道题中,一个字符串就是一件物品
    for (String str : strs) {
        // 获取这个字符串中 1 的个数和 0 的个数
        int zeroNum = countCharNum(str, '0');
        int oneNum = countCharNum(str, '1');
        // 内层循环遍历背包,这里有两个背包需要进行遍历,分别为容量为 m 的背包和 容量为 n 的背包,倒序遍历
        for (int i = m;i >= zeroNum;i--) {
            for (int j = n;j >= oneNum;j--) {
                // 使用递推公式计算 dp[i][j] 的值
                dp[i][j] = Math.max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
            }
        }
    }
    return dp[m][n];
}
private int countCharNum(String str, char ch) {
    int num = 0;
    for (int i = 0;i < str.length();i++) {
        if (str.charAt(i) == ch) {
            num++;
        }
    }
    return num;
}


完全背包问题


1.1、完全背包问题理论说明


1、题目描述


设有 n 种物品,每种物品都有一个重量及一个价值,其中第 i 件物品的重量为 weight[i],价值为 value[i]。且每种物品的数量是无限的,同时有一个背包,最大载重量为M ,今从 n 种物品种选取若干个物品(同一种物品可以被多次选取)放入背包中,使其重量的和小于等于 M ,而价值的和为最大。


与 01 背包不同的是,前者每件物品只有一个,而完全背包问题中每种物品可以有无限多个。


2、解题思路


在完全背包问题中,对于第 i 件物品,我们有几种选择:


1、选择将 0 个物品装入到背包中。


2、选择将 n 个该物品装入到背包中,我们可以不断往背包中添加该物品,直到背包被装满,也就是说,在完全背包问题中,同一件物品被装入背包的次数是有上下限的,下限为 0 ,上限为 M / n (向下取整)。


当前背包容量为 j 时,对于第 i 件物品装入背包的次数范围为 [0, j / w[i]] ,其中 j / w[i] 需要向下取整。


在之前使用一维滚动数组优化 01 背包问题时,我们有谈及到为什么内层循环需要进行反向遍历 – 为了避免一个物品被重复添加到背包中多次;而完全背包问题里的物品完全可以多次添加到背包中,所以对于完全背包问题的内层循环,我们可以正序遍历背包。


3、解题代码


/**
 * 求解完全背包问题
 * @param number 物品个数
 * @param capacity 背包容量
 * @param weight 物品重量数组
 * @param value 物品价值数组
 * @return 能获取到的最大收益
 */
public int completeBackpack(int number, int capacity, int[] weight, int[] value) {
    if (weight == null || value == null || weight.length != value.length) {
        return 0;
    }
    // 创建一个一维数组 dp
    int[] dp = new int[capacity + 1];
    // 外层数组遍历物品
    for (int i = 0;i < number;i++) {
        // 内层物品遍历背包,这里需要从左往右遍历
        for (int j = weight[i];j <= capacity;j++) {
            // 使用递推公式
            dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
        }
    }
    return dp[capacity];
}


1.2、零钱兑换


1、题目描述


给你一个整数数组 `coins` ,表示不同面额的硬币;以及一个整数 `amount` ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 ,
你可以认为每种硬币的数量是无限的。


示例一


输入:coins = [1, 2, 5], amount = 11
输出:3 
解释:11 = 5 + 5 + 1


示例二


输入:coins = [2], amount = 3
输出:-1


2、解题思路


这个问题可以看为一个完全背包问题,在这道题中,我们需要求解最值问题。


确定 dp 数组含义。


其中 dp[j] 表示,要想在 coins 中凑出金额为 j 的零钱,最少需要 dp[j] 个硬币。


状态转移方程

如果我们手一个 coins[i] ,那么我们只需要计算出凑出 j - coins[i] 的最少硬币数,然后 + 1,即可得到凑出 j 的最少硬币数,所以,我们得到的状态转移方程如下:


dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1);
// 注意,这里需要当子问题有解时才进行状态转移,即 dp[j - coins[i]] 不为 Integer.MAX_VALUE 时


初始化 dp 数组


1、当 j 为 0 时,此时只需要 0 个硬币就可以凑出,所以 dp[0] = 0;


2、当 j 不为 0 时,我们需要将其初始化为一个非常大的数,避免后面在求出一个大于 0 的数时,在经由 Math.min (0, dp[j]) 进行选择时,让 0 覆盖了结果值,比如说,我现在求出了 dp[1] 为 1 ,但是我的 dp[1] 在初始化时初始化为 0 了,所以导致我在进行取最小值时,无法将 dp[1] = 1 这个值正确地更新到数组中。


什么样的子问题才需要进行求解?

在我们进行遍历时,只有当子问题有解时,我们才考虑进行递推,所以,如果 dp[j - coin] 的值为 Integer.MAX_VALUE ,那么我们没必要进行状态转移,因为此时代表这个问题无解。


3、解题代码


public int coinChange(int[] coins, int amount) {
    if (coins == null || coins.length == 0) return -1;
    if (amount == 0) return 0;
    // 创建一个 dp 数组,长度为 amount + 1 ,其中 dp[amount] 就是在 coins 数组中,凑出 amount 所需的最少硬币数,也就是我们要求的结果
    int[] dp = new int[amount + 1];
    // 初始化 dp 数组,dp[0] = 1,这一步可以省略,我们需要将 dp 数组中非 0 位置的元素 初始化为 Integer.MAX_VALUE
    for (int i = 1;i < dp.length;i++) {
        dp[i] = Integer.MAX_VALUE;
    }
    // 外层遍历,遍历物品,也即硬币
    for (int coin : coins) {
        // 内层遍历,正向遍历背包,
        for (int j = coin;j <= amount;j++) {
            //  dp[j - coin] 不为 Integer.MAX_VALUE 时,证明此时 j - coin 可以被凑出,即这个问题有解,此时才需要进行求解。
            if (dp[j - coin] != Integer.MAX_VALUE) {
                // 使用状态转移方程求解 dp[j] 的值,这里求得是最小值
                dp[j] = Math.min(dp[j], dp[j - coin] + 1);
            }
        }
    }
    return dp[amount] == Integer.MAX_VALUE ? -1 : dp[amount];
}


1.3、零钱兑换 II


1、题目描述


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


请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 ,假设每一种面额的硬币有无限个。


示例一


输入:amount = 5, coins = [1, 2, 5]
输出:4
解释:有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1


示例二


输入:amount = 3, coins = [2]
输出:0
解释:只用面额 2 的硬币不能凑成总金额 3 。


2、解题思路


使用动态规划解决这道题,在本题中,由于每一种面额的硬币有无限个,我们将给定的 coins 数组看为物品数组,coins 数组中每个硬币的数额看为物品的重量,我们要做的就是寻找出凑齐 amount 重量的背包的方案数,这是一个完全背包问题。


确定 dp 数组的含义

dp[j] 指的是,在 coins 数组中,有 dp[j] 种凑出 amount 数值的方案。


状态转移方程

这是典型的组合问题,当我们手上有一颗数值为 coins[i] 的硬币时,此时我们只需要知道在 coins 中凑出 amount - nums[i] 的方案数,就可以得到该条件下的方案数,所以状态转移方程应该如下:


dp[j] += dp[j - nums[i]]


初始化 dp 数组,当 j 为 0 时,此时有一种方案可以凑出 0 ,所以:


dp[0] = 1;


3、解题代码


public int change(int amount, int[] coins) {
    if (coins == null || coins.length == 0) return 0;
    // 创建一个 dp 数组,其中 dp[j] 表示在 coins 中,能凑出数额为 j 的方案数
    int[] dp = new int[amount + 1];
    // 初始化 dp 数组,当 j 为 0 时, dp[j] == 1
    dp[0] = 1;
    // 外层遍历,遍历物品
    for (int i = 0;i < coins.length;i++) {
        // 内层遍历,对于完全背包问题,我们需要正向遍历背包
        for (int j = coins[i];j <= amount;j++) {
            // 使用递推公式计算出 dp[j]
            dp[j] += dp[j - coins[i]];
        }
    }
    return dp[amount];
}


1.4、组合总和 IV


1、题目描述


给你一个由 不同 整数组成的数组 `nums` ,和一个目标整数 `target` 。
请你从 `nums` 中找出并返回总和为 `target` 的元素组合的个数。
请注意,顺序不同的序列被视作不同的组合。


示例 一


输入:nums = [1,2,3], target = 4
输出:7
解释:
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。


示例 二


输入:nums = [9], target = 3
输出:0


2、解题思路


这道题是典型的完全背包问题,同时求的是排列数,所以外层循环需要遍历背包,内层循环需要遍历物品。


确定 dp 数组的含义。


dp[i] 表示凑成正整数为 i 的排列个数为 dp[i]。


确定递推公式

dp[i] 可以由 dp[i - nums[j]] 推导出来,其中dp[i] 考虑了 nums[j] ,而dp[i - nums[j]]不考虑 nums[j],因为只要得到 nums[j],排列个数 dp[i - nums[j]],就是 dp[i]的一部分。


我们可以确定递推公式如下:


dp[i] += dp[i - nums[j]];


初始化 dp 数组

由于递推公式为 dp[i] += dp[i - nums[j]] 的缘故,所以 dp[0] 要初始化为 1 ,这样递归其他 dp[i] 时才会有数值基础。


这里需要注意, dp[0] 是没有意义的。


3、解题代码


   public int combinationSum4(int[] nums, int target) {
       if (nums == null || nums.length == 0) return 0;
       // 创建一个 dp 数组,其中 dp[i] 表示能在 nums 中能组成 i 的排列数
       int[] dp = new int[target + 1];
// 初始化 dp[0] = 1 ,这个 dp[0] 是没有含义的
       dp[0] = 1;
       // 外层循环遍历背包
       for (int i = 0;i <= target;i++) {
           // 内层循环遍历物品
           for (int j = 0;j < nums.length;j++) {
               // 只有当背包容量大于第 j 件物品时,才需要进行分割子问题
               if (i >= nums[j]) {
                   dp[i] += dp[i - nums[j]];
               }
           }
       }
       return dp[target];
   }


1.5、爬楼梯


1、题目描述


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


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


示例一


输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1.  1 阶 + 1 阶
2.  2 阶


示例二


输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
1.  1 阶 + 1 阶 + 1 阶
2.  1 阶 + 2 阶
3.  2 阶 + 1 阶


2、解题思路


使用完全背包的思路解决这道题,这道题又是一道排列数的问题。


此时,背包中只有两件物品,即 1 和 2 ,我们需要找出装满容量为 n 的背包的方案数。


确定 dp 数组的含义。


其中 dp[i] 表示爬上 n 阶楼梯有 dp[i] 种不同的排列方案。


确定递推公式

由于此时球的是排列方法,所以我们可以得到本体的递推公式:


dp[i] += dp[i - nums[j]];


初始化 dp 数组

与上题相同,由于此时递推公式是 dp[i] += dp[i - nums[j]] ,所以我们需要初始 dp[0] = 1 ,这是递推的基础值。


本题中的物品

在本题中,只有两个物品,其中第一个物品的重量为 1 ,第二个物品的重量为 2。


3、解题代码


public int climbStairs(int n) {
    if (n == 1 || n == 2) return n;
    // 初始化一个 dp 数组,长度为 n + 1,其中 dp[i] 表示爬上 i 阶楼梯的方案排列数
    int[] dp = new int[n + 1];
    // 初始化 dp 数组
    dp[0] = 1;
    // 外层循环,在求排列的过程中需要遍历背包
    for (int i = 0;i <= n;i++) {
        // 内层循环,需要遍历物品,这里只有两件物品,其中第一件物品的重量为 1 ,第二件物品的重量为 2 
        for (int j = 1;j <= 2;j++) {
            // 如果当前背包的容量大于物品重量,这里为 j 
            if (i >= j) {
                dp[i] += dp[i - j];
            }
        }
    }
    return dp[n];
}


1.6、完全平方数


1、题目描述


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


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


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


示例一


输入:n = 12
输出:3 
解释:12 = 4 + 4 + 4


示例二


输入:n = 13
输出:2
解释:13 = 4 + 9


2、解题思路


这道题中,n 可以有多个相同的完全平方数 x^2 组成,也就是说,如果我们将 x 看为背包问题中的物品,那么每个 x 可以被多次加入到背包中。


我们可以将这道题转换为,给你一个容量为 n 的背包,同时给你一些物品,而物品由 n 决定,物品为 [1, b] ,其中满足 b * b <= n 且 (b + 1) * (b + 1) > n。


明确 dp 数组含义。


其中 dp[j] 表示 j 最少可以由 dp[j] 个完全平方数组成。


确定状态转移方程,这道题是一道求最值问题,所以状态转移方程为:


dp[j] = Math.min(dp[j], dp[j - i * i] + 1);


当我们手中握有 i * i 这个平方数时,我们只需要求出组成 j - i * i 最少需要多少个平方数,然后再这个结果的基础上加上一,就得到了我们想要的结果。


初始化 dp 数组

1、dp[0] 可以由 0 个平方数组成,所以 dp[0] = 0。


2、dp[1] 可以由一个平方数组成,所以dp[1] = 1。


3、对于其他下标的元素,在使用递推公式进行运算时,我们不希望它的真正结果被 0 覆盖,所以我们将其他下标的值都初始化为 Integer.MAX_VALUE。


在进行子问题分割时,只有当 dp[j - i * i] 不为 Integer.MAX_VALUE 时(此时子问题存在有效解),才进行递推公式的计算。


3、解题代码


private int getSizeByN(int n) {
    int size = 0;
    // 比如说 size 为 10 ,那么比 10 小的平方数有 1、4和9,可供选择的物品有 1 2 3 
    while (true) {
        // 如果 size + 1 的平方大于 n ,那么此时直接 break ,得到的 size 就是我们要寻找的值,当前物品的个数就是 1 - n 
        if ((size + 1) * (size + 1) > n) {
            break;
        }
        size++;
    }
    return size;
}


使用两个循环来解决这道题,先遍历物品后遍历背包,只有当当前背包容量大于 i * i 且子问题有效时,才能进行状态转移。


public int numSquares(int n) {
    if (n == 1) return 1;
    // 创建一个 dp 数组,其中 dp[i] 表示和为 i 的完全平方数的最少数量
    int[] dp = new int[n + 1];
    Arrays.fill(dp, Integer.MAX_VALUE);
    // 初始化 dp 数组
    dp[0] = 0;
    // 计算出当前物品的个数,即 size 
    int size = getSizeByN(n);
    // 外层循环遍历物品
    for (int i = 1;i <= size;i++) {
        // 内层循环正序遍历背包,只有当当前背包容量大于等于物品重量的平方时且子问题有解时,才能进行状态转移
        for (int j = 0;j <= n;j++) {
            if (j >= i * i && dp[j - i * i] != Integer.MAX_VALUE) {
                // 此时可以进行状态转移
                dp[j] = Math.min(dp[j], dp[j - i * i] + 1);
            }
        }
    }
    return dp[n];
}


背包问题总结

背包问题大体的接替模板是两层循环,分别是外层循环遍历物品,然后内层循环遍历背包,然后在循环中写状态转移方程。


在我现在的学习中,背包问题可以根据所给条件和待求项分为以下几类:


1.1、根据所给条件分类

根据所给物品的个数,可以分为 01 背包问题和完全背包问题,在使用一维数组进行空间优化时,二者的区别是内层循环的遍历顺序,其中:


对于 01 背包问题而言,内层循环在遍历背包时需要逆序遍历,保证物品只被加入到背包中一次。


对于完全背包问题而言,内层循环在遍历背包时需要正序遍历。


1.2、根据待求项分类

1、最值问题 I


问背包最多能装多少,状态转移方程一般如下:


dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);


**

2、存在问题**


当我们求解存在问题时,状态转移方程一般如下:


dp[j] = dp[j] || dp[j - nums[i]];


比如说上面我们就是将 分割等和子集 、 最后一块石头重量 II 看为存在问题解决。


3、组合问题


当我们求解存在问题时,状态转移方程一般如下:


dp[j] += dp[j - nums[i]];


比如说上面提到的零钱兑换 II 、 目标和 就是这类问题。


4、最值问题 II


问装满背包所有物品的最小个数,状态转移方程一般如下:


dp[j] = Math.min(dp[j], dp[j - nums[i]] + 1);


零钱问题 I 就是这样的问题。


1.3、组合问题和排列问题


1、组合问题


组合数的概念:从 n 个不同元素中,任取 m(m≤n)个元素不计顺序的组成一组,则所有不同的组合个数,称为组合数。


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


2、排列问题


排列数的概念:从 n 个不同元素中,任取 m(m≤n)个元素有顺序的排成一列,则所有不同的排列个数,称为排列数。


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


编辑距离问题总结

在使用动态规划求解字符串相关问题时, dp[i][j] 的含义通常是指字符串 s1 下标范围为 [0. i - 1] 的子串 a ,与字符串 s2 下标范围为 [0, j - 1] 的子串 b ,二者之间的某些指标。


个人觉得是为了考虑空串,同时为了 初始化工作的方便。


1.1、判断子序列

dp 数组含义:dp[i][j] 表示字符串 s 中范围下标为[0, i - 1] 的子串 a ,是否为字符串 t 中范围下标为[0, j - 1]的字符串 b 的子序列, dp 数组是一个二维的boolean 数组。


当s[i - 1] == t[j - 1]时,此时表示 s 与 t 找到了一个公共字符,此时我们只需要判断当前 s 的子串 a ,在去掉公共字符后,是否为去掉公共字符的 b 的子序列即可(其中 b 为 t 的子串)。


此时 s[0, i - 1] 是否为 t[0, j - 1]的子序列,这个结果完全由 s[0, i - 2] 与 t[0, j - 2]的关系决定。


如果此时 a 是 b 的子序列,那么证明s[0, i - 1] 是 t[0, j - 1]的子序列;反之则不是。


当s[i - 1] != t[j - 1],此时我们需要删去 t 子串中的最后一个字符 t[j - 1] ,因为此时 s[0, i - 1] 是否为 t[0,j - 1]的子序列与t[j - 1]完全没有关系。

如果 s[0, i - 1] 已经是 t[0, j - 2] 的子序列,那么 s[0, i - 1] 也必定为 t[0, j - 1] 的子序列。


如果 s[0, i - 1] 已经是 t[0, j - 2] 的子序列,那么 s[0, i - 1] 也必定为 t[0, j - 1] 的子序列。
如果 s[0, i - 1] 并不是 t[0, j - 2] 的子序列,
那么此时 t[j - 1] 这个字符于事无补,所以 s[0, i - 1] 也一定不为 t[0, j - 1] 的子序列。


本题递推公式


if (s[i - 1] == t[i - 1]) {
  dp[i][j] = dp[i - 1][j - 1]
} else {
  dp[i][j] = dp[i][j - 1]
}


1.2、不同的子序列

dp 数组含义:dp[i][j]表示字符串 s 中范围下标为[0, i - 1]的子串 a ,在字符串 t 中范围下标为[0, j - 1]的字符串 b 中作为子序列出现的次数,dp 数组是一个二维的 int 数组。


当 s[i - 1] == t[j - 1] 时,此时出现了公共字符,那么我们有两个选择:


1、第一个选择就是让 s[i - 1] 字符直接与 t[j - 1]进行匹配,比如说 s = bag , t = baagg,我们可以选择让 t 中的最后一个 g 与 s 的最后一个 g 进行匹配,这是其中一种选择,这种选择下得到的个数为dp[i][j] = dp[i - 1][j - 1] ,即我们只需要查找 s[0, i - 2]在t[0,j - 1] 中作为子序列出现的次数即可。


2、第二种选择就是让 s[i - 2]字符与 t[j - 1]进行匹配,而 s[i - 1]不参与匹配,比如说s = bag,t = baagg ,让 t 中倒数第二个 g 与 s 中的最后一个 g 进行匹配,这也是其中一种情况,这种情况下得到的个数为 dp[i][j] = dp[i][j - 1],我们需要查找 s[0. i - 1] 在t[0, j - 2]中作为子序列出现的次数。


由于本题中我们需要求出所有情况下出现次数之和,所以我们需要将上面两种选择得到的结果进行汇总。


当 s[i - 1] != t[j - 1] 时,此时我们只能舍弃 t 中最后一个字符,然后用剩下的字符进行匹配,此时式子为 dp[i][j] = dp[i][j - 1]。


递推公式


if (s[i - 1] == t[i - 1]) {
  dp[i][j] = dp[i - 1][j - 1];
} else {
  dp[i][j] = Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]) + 1, dp[i - 1][j - 1] + 2);
}


1.3、两个字符串中的删除操作

dp 数组含义:dp[i][j] 表示字符串 s 中范围下标为 [0, i - 1]的子串 a ,变为字符串 t 中范围下标为 [0, j - 1]的子串 b ,所需要进行的删除操作次数。


当 s[i - 1] == t[j - 1]时,此时我们此刻相等的字符为公共字符,从a[0, i - 1] 变为 b[0, j - 1]所需的最小删除次数与a[0, i - 2]变为b[0. j - 1]所需的最小删除次数相等,也就是说,公共字符的存在与否并不会对两个字符串的最小删除次数造成影响,所以我们可以得出此时的式子为 dp[i][j] = dp[i - 1][j - 1]。


当s[i - 1] == t[j - 1]时,此时我们用三种做法可供选择,此时我们假设 s 的子串为 a ,t 的子串为 b。


1、删去 a 中的某一个字符,此时得到的式子为 dp[i - 1][j] + 1,1 表示删去 a 中字符的那一次操作。


2、删去 b 中的某一个字符,此时得到的式子为 dp[i][j - 1] + 1 ,1 表示删去 b 中字符的那一次操作。


3、同时删去 a 和 b 中的字符,此时得到的式子为 dp[i - 1][j - 1] + 2。


由于我们要求最小删除次数,所以我们需要在上述的三种情况中选择一个最小的结果。


递推公式


if (s[i - 1] == t[i - 1]) {
  dp[i][j] = dp[i - 1][j - 1];
} else {
  dp[i][j] = Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1;
}


1.4、编辑距离

dp 数组含义:dp[i][j] 表示字符串 s 中范围下标为 [0, i - 1] 的子串 a ,与字符串 t 中范围下标为 [0, j - 1] 的子串 b 之间的最小编辑距离。


当 s[i - 1] == t[j - 1] 时,此时子串 a 和 子串 b 二者的编辑距离与去掉公共字符的编辑距离相等,即 dp[i][j] = dp[i - 1][j - 1]。


当 s[i - 1] == t[j - 1] 时,此时我们有几种做法可供选择:


1、word1 删除一个元素,那么就是以下标 word1[0, i - 2] 与 word2[0, j - 1]的最短编辑距离,再加上一个删除操作,此时得到的式子应该为 dp[i - 1][j] + 1。


2、word2 删除一个元素,那么就是以下标 word1[0, i - 1] 与 word2[0. j - 2] 的最短编辑距离,再加上一个删除操作,此时得到的式子应该为dp[i][j - 1] + 1。


3、替换元素,此时 word1 替换 word1[i - 1] ,使其等于 word2[j - 1] ,此时我们又可以将 word1[i - 1]看为一个公共元素,只不过我们需要原来的基础上加上一次编辑,此时得到的式子为 dp[i - 1][j - 1] + 1。


4、对于添加操作来说, word1 添加一个字符,就相当于 word2 删除一个字符,比如说,当 word1 为 abce ,word2 为 abc 时,word1 删去 e 与 word2 加上 e 的结果都相同,所以我们只需要考虑删除的情况即可。


递推公式


if (s[i - 1] == t[i - 1]) {
  dp[i][j] = dp[i - 1][j - 1];
} else {
  dp[i][j] = Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1;
}


相关文章
|
3天前
|
存储 索引 Python
python数据结构知识学习
【5月更文挑战第6天】Python提供四种核心数据结构:列表(List)——可变有序集合,支持索引和切片;元组(Tuple)——不可变有序集合;字典(Dictionary)——键值对结构,通过键访问值;集合(Set)——无序不重复元素集合,支持数学运算。此外,Python允许自定义数据结构,如链表、树、图,以适应不同问题需求。
13 0
|
8天前
|
机器学习/深度学习 算法 数据挖掘
【Python机器学习专栏】关联规则学习:Apriori算法详解
【4月更文挑战第30天】Apriori算法是一种用于关联规则学习的经典算法,尤其适用于购物篮分析,以发现商品间的购买关联。该算法基于支持度和置信度指标,通过迭代生成频繁项集并提取满足阈值的规则。Python中可借助mlxtend库实现Apriori,例如处理购物篮数据,设置支持度和置信度阈值,找出相关规则。
|
8天前
|
机器学习/深度学习 算法 前端开发
【Python机器学习专栏】集成学习算法的原理与应用
【4月更文挑战第30天】集成学习通过组合多个基学习器提升预测准确性,广泛应用于分类、回归等问题。主要步骤包括生成基学习器、训练和结合预测结果。算法类型有Bagging(如随机森林)、Boosting(如AdaBoost)和Stacking。Python中可使用scikit-learn实现,如示例代码展示的随机森林分类。集成学习能降低模型方差,缓解过拟合,提高预测性能。
|
20天前
|
算法
代码随想录算法训练营第五十六天 | LeetCode 647. 回文子串、516. 最长回文子序列、动态规划总结
代码随想录算法训练营第五十六天 | LeetCode 647. 回文子串、516. 最长回文子序列、动态规划总结
34 1
|
22天前
|
机器学习/深度学习 算法 前端开发
Scikit-learn进阶:探索集成学习算法
【4月更文挑战第17天】本文介绍了Scikit-learn中的集成学习算法,包括Bagging(如RandomForest)、Boosting(AdaBoost、GradientBoosting)和Stacking。通过结合多个学习器,集成学习能提高模型性能,减少偏差和方差。文中展示了如何使用Scikit-learn实现这些算法,并提供示例代码,帮助读者理解和应用集成学习提升模型预测准确性。
|
22天前
|
机器学习/深度学习 算法 Python
使用Python实现集成学习算法:Bagging与Boosting
使用Python实现集成学习算法:Bagging与Boosting
22 0
|
23天前
|
Python
python学习-函数模块,数据结构,字符串和列表(下)
python学习-函数模块,数据结构,字符串和列表
68 0
|
23天前
|
索引 Python
python学习-函数模块,数据结构,字符串和列表(上)
python学习-函数模块,数据结构,字符串和列表
29 0
|
23天前
|
算法 安全 数据可视化
python关联规则学习:FP-Growth算法对药品进行“菜篮子”分析
python关联规则学习:FP-Growth算法对药品进行“菜篮子”分析
|
27天前
|
算法
算法系列--动态规划--背包问题(5)--二维费用背包问题(下)
算法系列--动态规划--背包问题(5)--二维费用背包问题(下)
14 0