• 【LeetCode题目详解】第九章 动态规划 part05 1049. 最后一块石头的重量 II 494. 目标和 474.一和零(day43补)


    本文章代码以c++为例!

    一、力扣第1049题:最后一块石头的重量 II

    题目:

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

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

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

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

    示例 1:

    输入:stones = [2,7,4,1,8,1]
    输出:1
    解释:
    组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1],
    组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1],
    组合 2 和 1,得到 1,所以数组转化为 [1,1,1],
    组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。
    

    示例 2:

    输入:stones = [31,26,33,21,40]
    输出:5
    

    提示:

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

    思路

    如果对背包问题不都熟悉先看这两篇:

    本题其实就是尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小,这样就化解成01背包问题了

    是不是感觉和昨天讲解的416. 分割等和子集

    (opens new window)非常像了。

    本题物品的重量为stones[i],物品的价值也为stones[i]。

    对应着01背包里的物品重量weight[i]和 物品价值value[i]。

    接下来进行动规五步曲:

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

    dp[j]表示容量(这里说容量更形象,其实就是重量)为j的背包,最多可以背最大重量为dp[j]

    可以回忆一下01背包中,dp[j]的含义,容量为j的背包,最多可以装的价值为 dp[j]。

    相对于 01背包,本题中,石头的重量是 stones[i],石头的价值也是 stones[i] ,可以 “最多可以装的价值为 dp[j]” == “最多可以背的重量为dp[j]”

    1. 确定递推公式

    01背包的递推公式为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

    本题则是:dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);

    一些同学可能看到这dp[j - stones[i]] + stones[i]中 又有- stones[i] 又有+stones[i],看着有点晕乎。

    大家可以再去看 dp[j]的含义。

    1. dp数组如何初始化

    既然 dp[j]中的j表示容量,那么最大容量(重量)是多少呢,就是所有石头的重量和。

    因为提示中给出1 <= stones.length <= 30,1 <= stones[i] <= 1000,所以最大重量就是30 * 1000 。

    而我们要求的target其实只是最大重量的一半,所以dp数组开到15000大小就可以了。

    当然也可以把石头遍历一遍,计算出石头总重量 然后除2,得到dp数组的大小。

    我这里就直接用15000了。

    接下来就是如何初始化dp[j]呢,因为重量都不会是负数,所以dp[j]都初始化为0就可以了,这样在递归公式dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);中dp[j]才不会初始值所覆盖。

    代码为:

    vector<int> dp(15001, 0);
    
    1. 确定遍历顺序

    动态规划:关于01背包问题,你该了解这些!(滚动数组)

    (opens new window)中就已经说明:如果使用一维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历!

    代码如下:

    1. for (int i = 0; i < stones.size(); i++) { // 遍历物品
    2. for (int j = target; j >= stones[i]; j--) { // 遍历背包
    3. dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
    4. }
    5. }
    1. 举例推导dp数组

    举例,输入:[2,4,1,1],此时target = (2 + 4 + 1 + 1)/2 = 4 ,dp数组状态图如下:

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

    最后dp[target]里是容量为target的背包所能背的最大重量。

    那么分成两堆石头,一堆石头的总重量是dp[target],另一堆就是sum - dp[target]。

    在计算target的时候,target = sum / 2 因为是向下取整,所以sum - dp[target] 一定是大于等于dp[target]的

    那么相撞之后剩下的最小石头重量就是 (sum - dp[target]) - dp[target]。

    以上分析完毕,C++代码如下:

    1. class Solution {
    2. public:
    3. int lastStoneWeightII(vector<int>& stones) {
    4. vector<int> dp(15001, 0);
    5. int sum = 0;
    6. for (int i = 0; i < stones.size(); i++) sum += stones[i];
    7. int target = sum / 2;
    8. for (int i = 0; i < stones.size(); i++) { // 遍历物品
    9. for (int j = target; j >= stones[i]; j--) { // 遍历背包
    10. dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
    11. }
    12. }
    13. return sum - dp[target] - dp[target];
    14. }
    15. };
    • 时间复杂度:O(m × n) , m是石头总重量(准确的说是总重量的一半),n为石头块数
    • 空间复杂度:O(m)

    # 总结

    本题其实和416. 分割等和子集

    (opens new window)几乎是一样的,只是最后对dp[target]的处理方式不同。

    416. 分割等和子集

    (opens new window)相当于是求背包是否正好装满,而本题是求背包最多能装多少。

    二、力扣第494题:目标和

    题目:

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

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

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

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

    示例 1:

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

    示例 2:

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

    提示:

    • 1 <= nums.length <= 20
    • 0 <= nums[i] <= 1000
    • 0 <= sum(nums[i]) <= 1000
    • -1000 <= target <= 1000

    思路

    如果对背包问题不都熟悉先看这两篇:

    如果跟着「代码随想录」一起学过回溯算法系列

    (opens new window)的录友,看到这道题,应该有一种直觉,就是感觉好像回溯法可以爆搜出来。

    事实确实如此,下面我也会给出相应的代码,只不过会超时,哈哈。

    这道题目咋眼一看和动态规划背包啥的也没啥关系。

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

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

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

    公式来了, left - (sum - left) = target 推导出 left = (target + sum)/2 。

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

    此时问题就是在集合nums中找出和为left的组合。

    # 回溯算法

    在回溯算法系列中,一起学过这道题目回溯算法:39. 组合总和

    (opens new window)的录友应该感觉很熟悉,这不就是组合总和问题么?

    此时可以套组合总和的回溯法代码,几乎不用改动。

    当然,也可以转变成序列区间选+ 或者 -,使用回溯法,那就是另一个解法。

    我也把代码给出来吧,大家可以了解一下,回溯的解法,以下是本题转变为组合总和问题的回溯法代码:

    1. class Solution {
    2. private:
    3. vectorint>> result;
    4. vector<int> path;
    5. void backtracking(vector<int>& candidates, int target, int sum, int startIndex) {
    6. if (sum == target) {
    7. result.push_back(path);
    8. }
    9. // 如果 sum + candidates[i] > target 就终止遍历
    10. for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) {
    11. sum += candidates[i];
    12. path.push_back(candidates[i]);
    13. backtracking(candidates, target, sum, i + 1);
    14. sum -= candidates[i];
    15. path.pop_back();
    16. }
    17. }
    18. public:
    19. int findTargetSumWays(vector<int>& nums, int S) {
    20. int sum = 0;
    21. for (int i = 0; i < nums.size(); i++) sum += nums[i];
    22. if (S > sum) return 0; // 此时没有方案
    23. if ((S + sum) % 2) return 0; // 此时没有方案,两个int相加的时候要各位小心数值溢出的问题
    24. int bagSize = (S + sum) / 2; // 转变为组合总和问题,bagsize就是要求的和
    25. // 以下为回溯法代码
    26. result.clear();
    27. path.clear();
    28. sort(nums.begin(), nums.end()); // 需要排序
    29. backtracking(nums, bagSize, 0, 0);
    30. return result.size();
    31. }
    32. };

    当然以上代码超时了。

    也可以使用记忆化回溯,但这里我就不在回溯上下功夫了,直接看动规吧

    # 动态规划

    如何转化为01背包问题呢。

    假设加法的总和为x,那么减法对应的总和就是sum - x。

    所以我们要求的是 x - (sum - x) = target

    x = (target + sum) / 2

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

    这里的x,就是bagSize,也就是我们后面要求的背包容量。

    大家看到(target + sum) / 2 应该担心计算的过程中向下取整有没有影响。

    这么担心就对了,例如sum 是5,S是2的话其实就是无解的,所以:

    1. (C++代码中,输入的S 就是题目描述的 target)
    2. if ((S + sum) % 2 == 1) return 0; // 此时没有方案

    同时如果 S的绝对值已经大于sum,那么也是没有方案的。

    1. (C++代码中,输入的S 就是题目描述的 target)
    2. if (abs(S) > sum) return 0; // 此时没有方案

    再回归到01背包问题,为什么是01背包呢?

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

    这次和之前遇到的背包问题不一样了,之前都是求容量为j的背包,最多能装多少。

    本题则是装满有几种方法。其实这就是一个组合问题了。

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

    dp[j] 表示:填满j(包括j)这么大容积的包,有dp[j]种方法

    其实也可以使用二维dp数组来求解本题,dp[i][j]:使用 下标为[0, i]的nums[i]能够凑满j(包括j)这么大容量的包,有dp[i][j]种方法。

    下面我都是统一使用一维数组进行讲解, 二维降为一维(滚动数组),其实就是上一层拷贝下来,这个我在动态规划:关于01背包问题,你该了解这些!(滚动数组)

    (opens new window)也有介绍。

    1. 确定递推公式

    有哪些来源可以推出dp[j]呢?

    只要搞到nums[i],凑成dp[j]就有dp[j - nums[i]] 种方法。

    例如:dp[j],j 为5,

    • 已经有一个1(nums[i]) 的话,有 dp[4]种方法 凑成 容量为5的背包。
    • 已经有一个2(nums[i]) 的话,有 dp[3]种方法 凑成 容量为5的背包。
    • 已经有一个3(nums[i]) 的话,有 dp[2]中方法 凑成 容量为5的背包
    • 已经有一个4(nums[i]) 的话,有 dp[1]中方法 凑成 容量为5的背包
    • 已经有一个5 (nums[i])的话,有 dp[0]中方法 凑成 容量为5的背包

    那么凑整dp[5]有多少方法呢,也就是把 所有的 dp[j - nums[i]] 累加起来。

    所以求组合类问题的公式,都是类似这种:

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

    这个公式在后面在讲解背包解决排列组合问题的时候还会用到!

    1. dp数组如何初始化

    从递推公式可以看出,在初始化的时候dp[0] 一定要初始化为1,因为dp[0]是在公式中一切递推结果的起源,如果dp[0]是0的话,递推结果将都是0。

    这里有录友可能认为从dp数组定义来说 dp[0] 应该是0,也有录友认为dp[0]应该是1。

    其实不要硬去解释它的含义,咱就把 dp[0]的情况带入本题看看应该等于多少。

    如果数组[0] ,target = 0,那么 bagSize = (target + sum) / 2 = 0。 dp[0]也应该是1, 也就是说给数组里的元素 0 前面无论放加法还是减法,都是 1 种方法。

    所以本题我们应该初始化 dp[0] 为 1。

    可能有同学想了,那 如果是 数组[0,0,0,0,0] target = 0 呢。

    其实 此时最终的dp[0] = 32,也就是这五个零 子集的所有组合情况,但此dp[0]非彼dp[0],dp[0]能算出32,其基础是因为dp[0] = 1 累加起来的。

    dp[j]其他下标对应的数值也应该初始化为0,从递推公式也可以看出,dp[j]要保证是0的初始值,才能正确的由dp[j - nums[i]]推导出来。

    1. 确定遍历顺序

    动态规划:关于01背包问题,你该了解这些!(滚动数组)

    (opens new window)中,我们讲过对于01背包问题一维dp的遍历,nums放在外循环,target在内循环,且内循环倒序。

    1. 举例推导dp数组

    输入:nums: [1, 1, 1, 1, 1], S: 3

    bagSize = (S + sum) / 2 = (3 + 5) / 2 = 4

    dp数组状态变化如下:

    C++代码如下:

    1. class Solution {
    2. public:
    3. int findTargetSumWays(vector<int>& nums, int S) {
    4. int sum = 0;
    5. for (int i = 0; i < nums.size(); i++) sum += nums[i];
    6. if (abs(S) > sum) return 0; // 此时没有方案
    7. if ((S + sum) % 2 == 1) return 0; // 此时没有方案
    8. int bagSize = (S + sum) / 2;
    9. vector<int> dp(bagSize + 1, 0);
    10. dp[0] = 1;
    11. for (int i = 0; i < nums.size(); i++) {
    12. for (int j = bagSize; j >= nums[i]; j--) {
    13. dp[j] += dp[j - nums[i]];
    14. }
    15. }
    16. return dp[bagSize];
    17. }
    18. };
    • 时间复杂度:O(n × m),n为正数个数,m为背包容量
    • 空间复杂度:O(m),m为背包容量

    # 总结

    此时 大家应该不禁想起,我们之前讲过的回溯算法:39. 组合总和

    (opens new window)是不是应该也可以用dp来做啊?

    是的,如果仅仅是求个数的话,就可以用dp,但回溯算法:39. 组合总和

    (opens new window)要求的是把所有组合列出来,还是要使用回溯法爆搜的。

    本题还是有点难度,大家也可以记住,在求装满背包有几种方法的情况下,递推公式一般为:

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

    后面我们在讲解完全背包的时候,还会用到这个递推公式!

    三、力扣第474题:一和零

    题目:

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

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

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

    示例 1:

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

    示例 2:

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

    提示:

    • 1 <= strs.length <= 600
    • 1 <= strs[i].length <= 100
    • strs[i] 仅由 '0' 和 '1' 组成
    • 1 <= m, n <= 100

    思路

    如果对背包问题不都熟悉先看这两篇:

    这道题目,还是比较难的,也有点像程序员自己给自己出个脑筋急转弯,程序员何苦为难程序员呢。

    来说题,本题不少同学会认为是多重背包,一些题解也是这么写的。

    其实本题并不是多重背包,再来看一下这个图,捋清几种背包的关系

    416.分割等和子集1

    多重背包是每个物品,数量不同的情况。

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

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

    理解成多重背包的同学主要是把m和n混淆为物品了,感觉这是不同数量的物品,所以以为是多重背包。

    但本题其实是01背包问题!

    只不过这个背包有两个维度,一个是m 一个是n,而不同长度的字符串就是不同大小的待装物品。

    开始动规五部曲:

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

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

    1. 确定递推公式

    dp[i][j] 可以由前一个strs里的字符串推导出来,strs里的字符串有zeroNum个0,oneNum个1。

    dp[i][j] 就可以是 dp[i - zeroNum][j - oneNum] + 1。

    然后我们在遍历的过程中,取dp[i][j]的最大值。

    所以递推公式:dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);

    此时大家可以回想一下01背包的递推公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

    对比一下就会发现,字符串的zeroNum和oneNum相当于物品的重量(weight[i]),字符串本身的个数相当于物品的价值(value[i])。

    这就是一个典型的01背包! 只不过物品的重量有了两个维度而已。

    1. dp数组如何初始化

    动态规划:关于01背包问题,你该了解这些!(滚动数组)

    (opens new window)中已经讲解了,01背包的dp数组初始化为0就可以。

    因为物品价值不会是负数,初始为0,保证递推的时候dp[i][j]不会被初始值覆盖。

    1. 确定遍历顺序

    动态规划:关于01背包问题,你该了解这些!(滚动数组)

    (opens new window)中,我们讲到了01背包为什么一定是外层for循环遍历物品,内层for循环遍历背包容量且从后向前遍历!

    那么本题也是,物品就是strs里的字符串,背包容量就是题目描述中的m和n。

    代码如下:

    1. for (string str : strs) { // 遍历物品
    2. int oneNum = 0, zeroNum = 0;
    3. for (char c : str) {
    4. if (c == '0') zeroNum++;
    5. else oneNum++;
    6. }
    7. for (int i = m; i >= zeroNum; i--) { // 遍历背包容量且从后向前遍历!
    8. for (int j = n; j >= oneNum; j--) {
    9. dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
    10. }
    11. }
    12. }

    有同学可能想,那个遍历背包容量的两层for循环先后循序有没有什么讲究?

    没讲究,都是物品重量的一个维度,先遍历哪个都行!

    1. 举例推导dp数组

    以输入:["10","0001","111001","1","0"],m = 3,n = 3为例

    最后dp数组的状态如下所示:

    474.一和零

    以上动规五部曲分析完毕,C++代码如下:

    1. class Solution {
    2. public:
    3. int findMaxForm(vector& strs, int m, int n) {
    4. vectorint>> dp(m + 1, vector<int> (n + 1, 0)); // 默认初始化0
    5. for (string str : strs) { // 遍历物品
    6. int oneNum = 0, zeroNum = 0;
    7. for (char c : str) {
    8. if (c == '0') zeroNum++;
    9. else oneNum++;
    10. }
    11. for (int i = m; i >= zeroNum; i--) { // 遍历背包容量且从后向前遍历!
    12. for (int j = n; j >= oneNum; j--) {
    13. dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
    14. }
    15. }
    16. }
    17. return dp[m][n];
    18. }
    19. };
    • 时间复杂度: O(kmn),k 为strs的长度
    • 空间复杂度: O(mn)

    # 总结

    不少同学刷过这道题,可能没有总结这究竟是什么背包。

    此时我们讲解了0-1背包的多种应用,

    • (opens new window) 是求 给定背包容量,装满背包有多少种方法。
    • 本题是求 给定背包容量,装满背包最多有多少个物品。

    所以在刷的这些题目,都是 0-1背包不同维度上的应用,大家可以细心体会!

    day43补

  • 相关阅读:
    CMMI认证如何评估
    酷开科技OTT大屏营销,做好价值塑造
    GPT-4V:AI在医疗领域的应用
    JVM三色标记
    基于海鸥算法的无人机航迹规划-附代码
    微软宣布 Windows 11 开始大范围推送
    unity UGUI系统梳理 - Button
    ansible 执行速度优化参考 —— 筑梦之路
    【定位问题】基于matlab chan算法、fang算法、taylor算法求解目标定位问题【含Matlab源码 2135期】
    如何保障汽车嵌入式软件的质量与安全?您需要了解ASPICE标准
  • 原文地址:https://blog.csdn.net/weixin_67972246/article/details/132723058