算法Day42 | 背包概述,01背包,416.分割等和子集

背包问题

背包问题是一类经典的组合优化问题,基本形式是:给定一个固定大小的背包,和一些物品,每个物品具有自己的体积和价值,在不超过背包容量限制的前提下,将若干个物品装入背包,使得装入的物品总价值最大。

问题类型是否能选取多次物品是否分组
0-1 背包问题不可以
完全背包问题可以
多重背包问题有限制
混合背包问题有限制
分组背包问题可以

01背包

01背包问题是经典的动态规划问题,通常指的是在给定的一组物品中,选择若干个物品装入固定容量的背包,使得背包中物品的总价值最大。 "0-1"意味着每个物品只能选或不选,要么0,要么1。

二维 dp 数组

确认dp数组dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为 j 的背包,价值总和最大是多少。

递推公式
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

  • 不放物品 i:由dp[i - 1][j] 推出,即背包容量为 j,里面不放物品 i 的最大价值,此时 dp[i][j] 就是 dp[i - 1][j]。(其实就是当物品 i 的重量大于背包 j 的重量时,物品 i 无法放进背包中,所以背包内的价值依然和前面相同。)
  • 放物品 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]是由dp[i - 1] 推导出来的,因此 dp 数组中第一行需要初始化。

遍历顺序:正序遍历。先遍历物品,再遍历背包;或者先遍历背包,再遍历物品,都一样。因为dp[i]是由dp[i - 1] 推导出来的,是通过左上方推导的。

二维dp数组例子如下:

void test_demo1() {
    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;
}
滚动数组

根据递推公式 dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); ,可以将二维数组压缩到一维数组,将上一层拷贝到该层,通过实时更新数组的值,来实现与二维数组相同的作用。

确认dp数组:容量为 j 的背包,所背的物品价值可以最大为 dp[j]

递推公式
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

  • 不放物品 i:将上一层拷贝到该层,不更新dp[j]数组
  • 放物品 i:取dp[j - weight[i]] + value[i],即放物品 i ,结果为最大值

初始化dp[0] = 0为了服务于递推公式dp[非零] = 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]);
    }
}

只能先正序遍历物品,再倒序遍历背包。倒序遍历背包是为了保证每个物品只被添加一次

一维dp数组例子如下

void test_demo2() {
    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.back() << endl;
}

416.分割等和子集

题目链接:416.分割等和子集
问题转化为数组中是否有元素,求和能为sum / 2,再转化为容量为sum / 2 的背包,能否被装满。

例如,有数组[1, 5, 5, 11],物品容量分别为1, 5, 5, 11。背包容量为(1 + 5 + 5 + 11) / 2,问是否能装满背包。

递归公式为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i])

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int sum = 0;
        for (auto& num : nums) {
            sum += num;
        }
        if (sum % 2 == 1) return false;//和为奇数一定不满足
        
        vector<int> dp(sum / 2 + 1, 0);
        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]);//weight[i] == value[i]
            }
        }
        return dp.back() == sum / 2;
    }
};

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值