代码随想录算法训练营第四十二天|01背包问题,416. 分割等和子集

背包问题

背包问题一般有以下几类:
在这里插入图片描述
掌握01背包和完全背包即可。
先理解01背包。完全背包可以看作是01背包问题的变形。

01背包

什么是01背包问题?

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

举这样一个例子:
背包的最大重量为4。
物品为:
在这里插入图片描述
问:背包能背的物品最大价值是多少?

01背包 二维

我们依旧使用动态规划的五部曲来分析。
1、确定dp数组以及下标的含义。

定义一个dp[i][j]

这里直接写了dp[i][j],不明白为什么能这么写?是怎么想出来要这么定义的?为什么会有一个j?

该数组的含义是什么?
含义:任取下标为[0,i]之间的物品放入容量为j的背包里

或者可以理解为:
dp[i][j]表示背包容量为 j 时,从下标0至i的物品中选取,可以获得的最大价值。

在这里插入图片描述
2、确定递推公式
我们要思考,dp[i][j]这个结果可以从哪里得到?
背包的状态取决于放不放物品i。
对于任意一个物品,都只有两种状态,放和不放,物品i同样如此。

(1)不放物品i
不放物品i,从前i-1个物品中就得到了最优解。
即:背包容量为 j 时,从下标0至i-1的物品中选取,就能获得最大价值。
此时结果为:dp[i-1][j]

(2)放物品i
先写出表达式:
dp[i-1][j-weight[i]]+value[i]
其中,weights[i]表示第i个物品的重量,value[i]是第i个物品的价值。

我们现在要放物品i。因为要放物品i,那就不需要再遍历到i了。因为i已经确定要放入了,相当于一个前提条件,只需要从剩下的i-1个物品中再选即可,所以不需要遍历到i,只需要遍历到i-1。即任取物品的范围为[0, i-1]。

这种情况下物品i已经放入了背包中,背包的容量也要发生变化。此时我们要求的应该是已经放入物品i之后,剩余的重量还能放多少。因此背包的重量为j-weight[i]。
就有表达式:dp[i-1][j-weight[i]]
含义是:在背包容量为j-weight[i]的情况下,从下标为[0,i-1]的物品中任意选取,得到的最大价值。

dp[i][j]是从0至i的物品中选取,现在物品i已经放进去了,就要包括物品i的价值,因此要加上物品i的价值。

综上分析,就可以得到表达式:
dp[i-1][j-weight[i]]+value[i]

针对于情况(1)和情况(2),我们最终求的结果是最大的价值,因此最终的结果应该是两种情况中取得的最大值,谁的结果大就选哪一种情况。

因此递推公式为:
dp[i][j]=max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i])

3、初始化dp数组
对于这一部分,我是看了另外一个博主的文章讲解,这里就不写了。直接去看该博主的原文即可。

参考链接: https://www.cnblogs.com/DAYceng/p/17258797.html

4、确定遍历顺序
有两个遍历的维度:物品与背包重量。
先遍历谁都行。
先遍历物品再遍历背包重量更简单。
(1)先遍历物品再遍历背包
方向就是从左向右
在这里插入图片描述
固定物品0,去遍历背包,看看能不能放下?最大价值是多少。
只有背包容量为0的时候放不下,最大价值为0;背包容量1,2,3,4的时候都能放下物品0,最大价值均为15;

再固定物品1,去遍历背包。背包容量为0,1,2的时候放不下物品1,最大价值不变。背包容量为3,物品1可以替换原来的物品0,最大价值由原来的15变成了20。背包容量为4,这个时候物品0和物品1都可以放下,最大价值就更新为20+15=35。

其他位置的遍历分析同理。

(2)先遍历背包再遍历物品
方向就是从上到下
在这里插入图片描述
固定背包容量为0。所有的物品都装不下,最大价值为0。

固定背包容量为1。物品0可以装下,物品1和物品2都装不下,因此背包容量为1的时候最大价值为15。

固定背包容量为2。物品0可以装下,物品1和物品2都装不下,因此背包容量为1的时候最大价值为15。

固定背包容量为3。遍历到物品0,可以装下,此时最大价值为15;再遍历到物品1,发现物品1可以装下,就把物品0替换为物品1,最大价值也由15变成了20;遍历到物品2,无法装下,此时的最大价值还是20.

其他位置的遍历分析同理。

5、举例推导dp数组
如下图所示:
在这里插入图片描述
最优解(最大价值)是dp[2][4]。

代码实现

void test_2_wei_bag_problem1() {
    vector<int> weight = {1, 3, 4};
    vector<int> value = {15, 20, 30};
    int bagweight = 4;

    // 二维数组
    vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 0));

    // 初始化
    for (int j = weight[0]; j <= bagweight; j++) {
        dp[0][j] = value[0];
    }

    // weight数组的大小 就是物品个数
    for(int i = 1; i < weight.size(); i++) { // 遍历物品
        for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
            if (j < weight[i]) dp[i][j] = dp[i - 1][j];
            else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

        }
    }

    cout << dp[weight.size() - 1][bagweight] << endl;
}

int main() {
    test_2_wei_bag_problem1();
}

01背包 一维滚动数组

动态规划五部曲。
1、确定dp数组以及下标的含义。
d[j]:容量为j的背包,所背的物品的最大价值为dp[j]

2、确定递归公式
与二维时的情况类似,也分为放入物品i和不放入物品i这两种情况。
(1)不放入物品i
二维表达式为:dp[i-1][j]

一维表达式:dp[j]
dp[j]就还是取上一层自身的值

(2)放入物品i
二维表达式:
dp[i-1][j-weight[i]]+value[i]
一维表达式:
d[j-weight[i]]+value[i]

综上,递推公式为:
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
相对于二维dp数组的写法,就是把dp[i][j]中i的维度去掉了。

3、初始化dp数组
背包容量为0,则背包内的最大价值为0。
即dp[0]=0

其他位置如何初始化?
根据递推公式,我们总是去取最大值。因此,如果题目给的物品值均为正数,那dp[0]以外的位置应该初始化为0,这样才可以保证在递推过程中,判断累加所得的最大值不会被初始值覆盖

举个例子,当j移动到5处时,即dp[5],如果此前所背物品价值累加为10,而当前dp[5]的初始值是100,就会把之前的值覆盖掉。

所以,在创建dp数组的时候,把所有的元素都初始化为0就行。

4、确定遍历顺序
一维的遍历顺序和二维的有很大的区别。

一维的遍历,需要先遍历物品再遍历背包,同时遍历背包需要倒序遍历。

为什么需要倒序遍历?

倒序遍历是为了保证物品只被放入了一次。

比如文章开头的例子:
在这里插入图片描述
物品0的重量为weight[0] = 1,价值value[0] = 15
如果使用正序遍历:
dp[0] = 0;---初始化是0
dp[1] = dp[1 - weight[0]] + value[0] = 15;
dp[2] = dp[2 - weight[0]] + value[0] = 30;

当j为1时,表示容量为1,此时能够放下一个物品0,根据递推公式我们应该让 dp[j]等于dp[j - weight[i]] + value[i],即需要放入物品,因此有了上述式子。

当j为2时,容量为2,根据递推公式此时确实需要放入物品,因为当前层容量够。
但是,由于遍历顺序是正序遍历,在计算dp[2]时会把dp[1]的结果累加进来,这显然是错误的,因为每个物品只能放一次。
所以正序遍历有问题。

如果使用倒序遍历:
dp[2] = dp[2 - weight[0]] + value[0] = 15;
dp[1] = dp[1 - weight[0]] + value[0] = 15;
dp[0] = 0;---初始化是0
结果是正常的。

个人理解是后面的结果需要前面的原值来更新,后序遍历可以保证前面的值不变,前序遍历会让前面的值变。

5、举例推导dp数组
如下图所示:
在这里插入图片描述

代码实现

void test_1_wei_bag_problem() {
    vector<int> weight = {1, 3, 4};
    vector<int> value = {15, 20, 30};
    int bagWeight = 4;

    // 初始化
    vector<int> dp(bagWeight + 1, 0);
    for(int i = 0; i < weight.size(); i++) { // 遍历物品
        for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
            dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
        }
    }
    cout << dp[bagWeight] << endl;
}

int main() {
    test_1_wei_bag_problem();
}

题目:416. 分割等和子集

题目描述

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

在这里插入图片描述

题目链接/讲解链接:
https://programmercarl.com/0416.%E5%88%86%E5%89%B2%E7%AD%89%E5%92%8C%E5%AD%90%E9%9B%86.html

思路

在这里插入图片描述

解题

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int sum = 0;

        // dp[i]中的i表示背包内总和
        // 题目中说:每个数组中的元素不会超过 100,数组的大小不会超过 200
        // 总和不会大于20000,背包最大只需要其中一半,所以10001大小就可以了
        vector<int> dp(10001, 0);
        for (int i = 0; i < nums.size(); i++) {
            sum += nums[i];
        }
        // 也可以使用库函数一步求和
        // int sum = accumulate(nums.begin(), nums.end(), 0);
        if (sum % 2 == 1) return false;
        int target = sum / 2;

        // 开始 01背包
        for(int i = 0; i < nums.size(); i++) {
            for(int j = target; j >= nums[i]; j--) { // 每一个元素一定是不可重复放入,所以从大到小遍历
                dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
            }
        }
        // 集合中的元素正好可以凑成总和target
        if (dp[target] == target) return true;
        return false;
    }
};
  • 28
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值