Day43|动态规划part05: 1049. 最后一块石头的重量 II、494. 目标和、474. 一和零

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

leetcode链接:力扣题目链接

视频链接:这个背包最多能装多少?LeetCode: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
解释:
组合 24,得到 2,所以数组转化为 [2,7,1,8,1],
组合 78,得到 1,所以数组转化为 [2,1,1,1],
组合 21,得到 1,所以数组转化为 [1,1,1],
组合 11,得到 0,所以数组转化为 [1],这就是最优值。
示例 2:

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

本题的思路是尽量找到重量相近的两个石头两两配对,尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小,这样就化解成01背包问题了

本题物品的重量为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. 确定递推公式

跟划分相同子集那题一样,dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

  1. dp数组的初始化

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

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

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

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

这里就直接用15000了。

初始化dp[j],直接初始化为0即可。

  1. 确定遍历顺序

跟之前的一样,还是从后往前:

for(int i = 0 ; i < stones.size(); i++){
            for(int j = target; j >= stones[i]; j--){
                dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
            }
        }
        
  1. 打印DP数组

Day43|动态规划part05: 1049. 最后一块石头的重量 II、494. 目标和、474. 一和零_第1张图片

  1. 与01背包进行转化(自己增加的)

这一步就是如何将01背包的结果与原题联系起来。因为我们最终得到的是一个dp数组。

最终得到的dp[target]是容量为target的背包所能背的最大重量,也就是分成的第一堆的重量,第二堆的重量就是sum - dp[target],又因为sum / 2是向下取整的返回较小的那个,因此第二堆重要一定大于等于第一堆。故最终留下的最小石头重量为:

return sum - dp[target] - dp[target];

最终代码:

class Solution {
public:
    int lastStoneWeightII(vector<int>& stones) {
        int sum = 0;
        for(int stone: stones){
            sum += stone;
        }
        int target = sum / 2;
        vector<int> dp(target + 1,0);
        for(int i = 0 ; i < stones.size(); i++){
            for(int j = target; j >= stones[i]; j--){
                dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
            }
        }
        return sum - dp[target] - dp[target];

    }
};

494.目标和(二刷使用回溯再做一遍)

leetcode链接:力扣题目链接

视频链接:装满背包有多少种方法?| LeetCode: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

这题乍一看,其实可以使用回溯来做,进行暴力搜索,但最后会超时,二刷复习的时候再用回溯做一遍。

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

假设加法的总和为x,那么减法对应的总和(绝对值,也就是减数)就是sum - x。

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

x = (target + sum) / 2

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

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

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

  1. 确定递推公式

只要搞到nums[i],凑成dp[j]就有dp[j - nums[i]] 种方法。也就是说dp[j - nums[i]]渴望找到nums[i],这样就符合要求了。因此递推公式是:

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

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

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

  1. dp数组的初始化

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

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

其他的元素由dp[0]递推而来。

  1. 确定遍历顺序

跟之前的一维0-1背包一样,先遍历物品再遍历背包,背包从后往前遍历。

  1. 举例推导dp数组

Day43|动态规划part05: 1049. 最后一块石头的重量 II、494. 目标和、474. 一和零_第2张图片

最终代码:

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int sum = 0;
        for(int num : nums){
            sum += num;
        }
        if((sum + target) % 2 != 0){
            return 0;
        }
        if(abs(target) > sum){
            return 0;
        }
        int x = (sum + target) / 2;
        vector<int> dp(x + 1, 0);
        dp[0] = 1;
        for(int i = 0; i < nums.size(); i++){
            for(int j = x; j >= nums[i]; j--){
                dp[j] += dp[j - nums[i]];
            }
        }
        return dp[x];
    }
};

474.一和零

leetcode链接:力扣题目链接

视频链接:装满这个背包最多用多少个物品?| LeetCode:474.一和

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

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

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

示例 1:

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

示例 2:

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

这道题表面上是多重背包,实际上是01背包问题。

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

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

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

  1. 确定DP数组下标及其含义

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数组的初始化

全部初始化为0即可。

  1. 确定遍历顺序

本题同一维背包一样(注意,这里虽然dp是二维数组,但是这里的二维标识的是一个物品的不同质量,因此总的还说跟一维的一样的),还是内层从后往前:

for (string str : strs) { // 遍历物品
    int oneNum = 0, zeroNum = 0;
    for (char c : str) {
        if (c == '0') zeroNum++;
        else oneNum++;
    }
    for (int i = m; i >= zeroNum; i--) { // 遍历背包容量且从后向前遍历!
        for (int j = n; j >= oneNum; j--) {
            dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
        }
    }
}
  1. 举例推导dp数组

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

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

Day43|动态规划part05: 1049. 最后一块石头的重量 II、494. 目标和、474. 一和零_第3张图片

最终代码:

class Solution {
public:
    int findMaxForm(vector& strs, int m, int n) {
        vector> dp(m + 1, vector (n + 1, 0)); // 默认初始化0
        for (string str : strs) { // 遍历物品
            int oneNum = 0, zeroNum = 0;
            for (char c : str) {
                if (c == '0') zeroNum++;
                else oneNum++;
            }
            for (int i = m; i >= zeroNum; i--) { // 遍历背包容量且从后向前遍历!
                for (int j = n; j >= oneNum; j--) {
                    dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
                }
            }
        }
        return dp[m][n];
    }
};
  • 时间复杂度: O(kmn),k 为strs的长度
  • 空间复杂度: O(mn)

总结

  • 掌握了背包之后,怎么把其他问题抽象成背包问题是关键;
  • 求凑到对应target的数量的问题可以转化成背包问题。
  • 对于dp数组,有的时候表示的是最大容量,有的时候表示装的方法,要具体问题具体对待。

你可能感兴趣的:(数据结构与算法(一刷),动态规划,算法)