Day 42 0-1背包理论基础 416. 分割等和子集

01背包理论基础

先了解背包问题的区别和分类:

​ 由于所有的问题的原理都可以转化为01背包;通过纯01背包问题,把01背包原理讲清楚;

01背包

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

​ 先考虑暴力解法:

​ 每一件物品其实只有两个状态,取或者不取,所以可以使用回溯法搜索出所有的情况,那么时间复杂度就是 o ( 2 n ) o(2^n) o(2n),这里的n表示物品数量;

​ 举例说明:

​ 背包最大重量为4。

​ 物品为:

重量价值
物品0115
物品1320
物品2430

​ 问背包能背的物品最大价值是多少?

二维dp数组实现

​ 依然动规五部曲分析一波。

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

​ 对于01背包问题,有两种写法, 这里先使用二维数组,即dp[i][j] 表示从下标为[0-i]的物品类里选择(此题是因为每个目标只有一个的01背包,所以不用考虑这个类,涉及到多个的问题时,物品类的这个“类”就很重要了),放进容量为j的背包,其最终价值总和最大是多少;只看这个二维数组的定义,一定会很懵,看下面这个图:

动态规划-背包问题1

要时刻记着这个dp数组的含义,下面的一些步骤都围绕这dp数组的含义进行的

​ 如果哪里看懵了,就来回顾一下i代表什么,j又代表什么,dp[i][j]又代表什么;

​ 2.确定递推公式:

​ 考虑dp[i][j]的含义:从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少;

​ 可以从两个方向推出来dp[i][j]:

已经放了前0到i - 1个物品,不放物品i:由dp[i - 1][j]推出,即背包容量为j,里面不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j]。(其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以背包内的价值依然和前面相同)

已经放了前0到i - 1个物品,再放物品i:由dp[i - 1][j - weight[i]]推出,dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]的时候不放物品i的最大价值,那么dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值

​ 所以递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

​ 3.dp数组如何初始化

关于初始化,一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱

​ 首先从dp[i][j]的定义出发,如果背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为0。

​ 如图:

​ 所以第一列必然是全为0,因为根本放不进东西进去;

​ 再看递推公式:

​ dp[i][j] = max{dp[i - 1][j], dp[i - 1][j - weight[j]] +value[i]};

​ 可以看出,dp[i][j]要么是由正上方,要么是由左上方推出来;

​ 分析第一行:

​ dp[0][j],即:i为0,存放编号0的物品的时候,各个容量的背包所能存放的最大价值。

​ 那么很明显当 j < weight[0]的时候,dp[0][j] 应该是 0,因为背包容量比编号0的物品重量还小。

​ 当j >= weight[0]时,dp[0][j] 应该是value[0],因为背包容量放足够放编号0物品。

​ 初始化代码即如下所示:

	for (int j = 0 ; j < weight[0]; j++) {  
        // 当然这一步,如果把dp数组预先初始化为0了,这一步就可以省略,但很多同学应该没有想清楚这一点。
    	dp[0][j] = 0;
	}
	// 正序遍历
	for (int j = weight[0]; j <= maxweight; j++) {
    	dp[0][j] = value[0];
	}

​ 如本例,初始化就应该如下:

​ 至于其他部分的初始化,由于dp[i][j]的所有其他部分都可以由第一排和第一列退出,所以数组整体无论怎么初始化都行,只要是一个符合范围的合法值即可;为了代码简洁性,此处统一初始化为0;

​ 所以最终初始化代码如下:

	vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 0));//这一步其实已经包含了第一列的初始化
	for (int j = weight[0]; j <= bagweight; j++) {
    	dp[0][j] = value[0];
	}

​ 4.确定遍历顺序

​ 从上述条件可以看出,遍历维度有两个维度:

​ 物品和背包重量;

​ 那么就是一个考虑遍历先后顺序的问题;

​ 假设先遍历物品,再遍历背包重量

	// 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]);
    	}
	}

​ 按照上述代码,实现如下:

先遍历背包,再遍历物品(注意仅针对二维dp数组01背包的情况实用):

	// weight数组的大小 就是物品个数
	for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
    	for(int i = 1; i < weight.size(); i++) { // 遍历物品
        	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]);
    	}
	}

​ 实现如下:

​ 5.打印dp数组

​ 举例推导dp数组

#include<iostream>
#include<vector>
using namespace std;

int n, bagspace;
void test() {
	vector<int> weight(n, 0);
	vector<int> value(n, 0);
    cout << "Please enter the weight array(size is n):" << endl;
    for (int i = 0; i < n; ++i) {
        cin >> weight[i];
    }
    cout << "Please enter the value array(size is n):" << endl;
    for (int j = 0; j < n; ++j) {
        cin >> value[j];
    }
    // dp数组, dp[i][j]代表行李箱空间为j的情况下,从下标为[0, i]的物品里面任意取,能达到的最大价值
    vector<vector<int>> dp(weight.size(), vector<int>(bagspace + 1, 0));

    //初始化,由于已经初始化了dp[i][0](第一列全为0)
    //那么接下来只需要初始化第一行
    //由于当j<bagsapce的时候dp[0][j]=0,所以j可以从weight[0]开取
    for (int j = weight[0]; j < bagspace; j++) {
        dp[0][j] = value[0];
    }

    for (int i = 1; i < weight.size(); i++) {
        for (int j = 0; j <= bagspace; j++) {
            // 如果装不下这个物品,那么就继承dp[i - 1][j]的值
            if (j < weight[i]) dp[i][j] = dp[i - 1][j];
            // 如果能装下,就将值更新为 不装这个物品的最大值 和 装这个物品的最大值 中的 最大值
            // 装这个物品的最大值由容量为j - weight[i]的包任意放入序号为[0, i - 1]的最大值 + 该物品的价值构成
            else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
        }
    }
    cout << dp[weight.size() - 1][bagspace] << endl;
}
int main() {
    cout << "Please enter the numbers of objects and space of the bag:" << endl;
    while (cin >> n >> bagspace) {
        test();
    }
    return 0;
}

一维dp数组实现(滚动数组)

​ 由于dp数组的状态其实是可以压缩的,由二维数组的递推公式:

​ dp[i][j] = max{dp[i - 1][j] , dp[i][j - weight[i]] +value[i]};

​ 考虑将dp[i-1]这层拷贝到dp[i]这一层上,其实只用一维数组就可以实现了;

​ 再次回顾dp[i][j] 的含义

​ i是物品,j是背包容量,dp[i][j]是任取第[0, i ]个物品,放进容量为j的背包能达到的价值最大值;

​ 动态规划五部曲:

​ 1.确定dp数组含义:

​ dp[j]即背包容量为j的背包,能达到的价值最大值dp[j];

​ 2.dp数组的递推公式:
​ dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); 其实很好理解,就是把i的维度去掉了;

​ 3.dp数组的初始化:

​ 显然,dp[0] = 0;由递推公式,可以知道dp[j]是有一个比较取最大值的过程,所以为了避免获取值被初始值覆盖,此时取INT_32MIN是最合适的,这里假设背包容量均大于0,可以全部初始化为零;

​ 4.dp数组遍历顺序:

注意,以为和二维最大的区别就在遍历顺序上

	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]);
    	}
	}

​ 首先是先遍历物品,再遍历背包;很显然这其实就是对于上面的二维数组的一层一层地遍历;

这里是不能更改for循环的嵌套顺序的;因为一旦先遍历背包,那么背包只会放进一个物品;

​ 其次就是在遍历背包的时候,为了防止物品多次选取,需要倒叙遍历;

​ 一旦正序遍历了,那么物品0就会被重复加入多次!

​ 举例:物品0的重量weight[0] = 1,价值value[0] = 15

​ 如果正序遍历

​ dp[1] = dp[1 - weight[0]] + value[0] = 15

​ dp[2] = dp[2 - weight[0]] + value[0] = 30

​ 此时dp[2]就已经是30了,意味着物品0,被放入了两次,所以不能正序遍历;

​ 由于这里所有dp[j]都等于0,正好满足倒序情况:

​ dp[2] = dp[2 - weight[0]] + value[0] = 15 (dp数组已经都初始化为0)

​ dp[1] = dp[1 - weight[0]] + value[0] = 15

​ 5.打印dp数组:

void test02() {
    vector<int> weight(n, 0);
    vector<int> value(n, 0);
    cout << "Please enter the weight array(size is n):" << endl;
    for (int i = 0; i < n; ++i) {
        cin >> weight[i];
    }
    cout << "Please enter the value array(size is n):" << endl;
    for (int j = 0; j < n; ++j) {
        cin >> value[j];
    }
    vector<int> dp(bagspace + 1, 0);//初始化已经完成

    for (int i = 0; i < weight.size(); i++) {
        cout << "This is current dp array:" << endl;
        for (int j = bagspace; j >= weight[i]; j--) {
            dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
        }
        for (int k = 0; k < bagspace + 1; k++) {
            cout << dp[k] << ' ';
        }
        cout << endl;
    }
    cout << dp[bagspace] << endl;
}

分割等和子集

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

注意: 每个数组中的元素不会超过 100 数组的大小不会超过 200

示例 1:

  • 输入: [1, 5, 11, 5]
  • 输出: true
  • 解释: 数组可以分割成 [1, 5, 5] 和 [11].

示例 2:

  • 输入: [1, 2, 3, 5]
  • 输出: false
  • 解释: 数组不能分割成两个元素和相等的子集.

提示:

  • 1 <= nums.length <= 200
  • 1 <= nums[i] <= 100

​ 直觉第一想法自然是回溯,但是0-1背包都学了,不能不用吧)

​ 首先确定,本题的每个元素只能取一次,而不是可以重复取,所以考虑0-1背包;

​ 其次背包的weight和value均是数值本身,且背包的体积大小为sum/2,只要当背包的体积刚好为满,则返回true;

​ 思路明确,动规五部曲:

​ 1.确定dp数组含义:

​ dp[j],表示当背包容量(正整数之和)为j的时候,背包所取的最大价值(其实还是正整数之和)

​ 2.确定递推公式:

​ 对于0-1背包,dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

​ 带入此题就是,dp[j] = max(dp[j], dp[j - nums[i]] +nums[i]);

​ 3.dp数组初始化:

​ dp[0]肯定是0;

​ 如果题目给的价值都是正整数那么非0下标都初始化为0就可以了,如果题目给的价值有负数,那么非0下标就要初始化为负无穷;

这样才能让dp数组在递推的过程中取得最大的价值,而不是被初始值覆盖了

​ 本题题目中 只包含正整数的非空数组,所以非0下标的元素初始化为0就可以了;

​ 即vector<int> dp(10001, 0);

​ 4.遍历顺序:

​ 从后向前

for(int i = 0; i < nums.size(); i++) {
    for(int j = sum/2; j >= nums[i]; j--) { // 每一个元素一定是不可重复放入,所以从大到小遍历
        dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
    }
}

​ 5.打印dp数组

​ 以题例为例

image-20240509001125123

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int sum = accumulate(nums.begin(), nums.end(), 0);
        if(sum % 2 == 1)  return false;//sum为奇数
        int target = sum/2;
        vector<int> dp(10001, 0);
        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]);
            }
        }
        if(dp[target] == target)    return true;
        return false;
    }
};
  • 22
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值