• 算法训练day43|动态规划 part05:0-1背包 (LeetCode 1049. 最后一块石头的重量 II、494. 目标和、474.一和零)


    1049. 最后一块石头的重量 II (求最大重量)

    题目链接🔥🔥
    有一堆石头,每块石头的重量都是正整数。
    每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
    如果 x == y,那么两块石头都会被完全粉碎;
    如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
    最后,最多只会剩下一块石头。返回此石头最小的可能重量。如果没有石头剩下,就返回 0。

    示例:
    输入:[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],这就是最优值。

    提示:
    1 <= stones.length <= 30
    1 <= stones[i] <= 1000

    思路分析

    本题其实就是尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小,这样就化解成01背包问题了。
    并且和分割等和子集很像了。

    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表示容量,那么最大容量(重量)是多少呢,就是所有石头的重量和的一半。
    因为重量都不会是负数,所以dp[j]都初始化为0就可以了,这样在递归公式dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);中dp[j]才不会初始值所覆盖。

    1. 确定遍历顺序

    如果使用一维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历!

    1. 举例推导dp数组
      在这里插入图片描述

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

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

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

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

    代码实现

    class Solution {
    public:
        int lastStoneWeightII(vector<int>& stones) {
            int sum=0;
            for(int i:stones){
                sum+=i;
            }
            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]);
                }
            }
            int result=sum-2*dp[target];
            return result;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    494. 目标和 (求组合方法数)

    题目链接🔥🔥

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

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

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

    提示:
    数组非空,且长度不会超过 20 。
    初始的数组的和不会超过 1000 。
    保证返回的最终结果能被 32 位整数存下

    思路分析

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

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

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

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

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

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

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

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

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

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

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

    if (abs(target) > sum) return 0; // 此时没有方案
    
    • 1

    动规方法

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

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

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

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

    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

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

    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背包问题一维dp的遍历,nums放在外循环,target在内循环,且内循环倒序。

    1. 举例推导dp数组

    输入:nums: [1, 1, 1, 1, 1], S: 3
    bagSize = (S + sum) / 2 = (3 + 5) / 2 = 4
    dp数组状态变化如下:
    在这里插入图片描述

    代码实现

    class Solution {
    public:
        int findTargetSumWays(vector<int>& nums, int target) {
            int sum=0;
            for(int i:nums) sum+=i;
            if(abs(target)>sum) return 0;
            if((target+sum)%2) return 0;
            int bagsize=(target+sum)/2;
            vector<int> dp(bagsize+1,0);
            dp[0]=1;
            for(int i=0;i<nums.size();i++){
                for(int j=bagsize;j>=nums[i];j--){
                    dp[j]+=dp[j-nums[i]];
                }
            }
            return dp[bagsize];
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    总结思考

    本题还是有点难度,大家也可以记住,在求装满背包有几种方法(仅仅是求个数,不用把所有组合列出来)的情况下,递推公式一般为:

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

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


    474.一和零 (求二维背包的最大物品数)

    题目链接🔥🔥
    给你一个二进制字符串数组 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 。

    示例 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

    思路分析

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

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

    而m 和 n相当于是一个背包,两个维度的背包。本题其实还是01背包问题

    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背包一定是外层for循环遍历物品,内层for循环遍历背包容量且从后向前遍历!

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

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

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

    1. 举例推导dp数组

    在这里插入图片描述

    代码实现

    class Solution {
    public:
        int findMaxForm(vector<string>& strs, int m, int n) {
            vector<vector<int>> dp(m+1,vector<int> (n+1,0));
            for(string str:strs){ // 遍历物品
                int zeronums=0,onenums=0;
                for(char c:str){
                    if(c=='0') zeronums++;
                    else onenums++;
                }
                for(int i=m;i>=zeronums;i--){  // 遍历背包容量且从后向前遍历!
                    for(int j=n;j>=onenums;j--){
                        dp[i][j]=max(dp[i][j],dp[i-zeronums][j-onenums]+1);
                    }
                }
            }
            return dp[m][n];
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    思考总结

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

    纯0-1背包 是求 给定背包容量 装满背包 的最大价值是多少。
    416. 分割等和子集 是求 给定背包容量,能不能装满这个背包。
    1049. 最后一块石头的重量 II 是求 给定背包容量,尽可能装,最多能装多少
    494. 目标和是求 给定背包容量,装满背包有多少种方法。
    本题是求 给定背包容量,装满背包最多有多少个物品。


  • 相关阅读:
    全志 Android 11:实现响应全局按键
    LeetCode --- 1356. Sort Integers by The Number of 1 Bits 解题报告
    SH学习1
    py-运算符
    解决 pdf.js 出现 TypeError: key.split(...).at is not a function 报错问题
    八股文总结
    【Unity程序技巧】2D音乐中心管理器
    【模糊综合评价的运用】——《电子舌技术在食用盐模糊感官评价中的应用》论文笔记(内附MATLAB程序)
    Jmeter常用线程组设置策略
    CSS和HTML结合使用的三种方式
  • 原文地址:https://blog.csdn.net/weixin_43399263/article/details/132719644