《dp补卡——01背包问题》

69 阅读4分钟

在这里插入图片描述\

目录

01背包

1、dp数组以及下标含义

dp[i][j]标识从下标为[0,1]的物品里任意取,放进容量为j的背包,价值总和最大是多少?

在这里插入图片描述

2、确定递推公式

dp[i][j]可以由两个方向推出:

1、dp[i-1][j],背包容量为j,里面不放入物品i的最大价值,此时dp[i][j] = dp[i-1][j]

2、dp[i-1][i-weight[i]]推出,背包容量为i-weight[i]的时候此时dp[i][j] = dp[i-1][j-weight[i]]+valuep[i];

所以递推公式为:

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

3、dp数组如何初始化

关于初始化,一定要和dp数组的定义吻合。

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

由递推可知i是由i-1推出来的,那么i为0时一定要初始化。

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

for(int j = bagWeight; j >= weight[0]; j--)
{
    dp[0][j] = dp[0][j-weight[0]] + value[0];
}

这里需要注意,初始化是倒序遍历。

dp[0][j]表示容量为j的背包存放物品0时候的最大价值。由于每个物品只有1个,如果dp[0][j]必须为初值,正序遍历,物品0会被重复加入多次。

dp[i][j]在推导的时候一定是取价值最大的数,如果题目给的价值都是正整数,那么下标初始化为0.如果价值里面有负数,初始化为负无穷。只要保证dp数组在递推公式的过程中取最大的价值,而不是被初始值覆盖。

所以dp数组初始化如下:

vector<vector<int>> dp(weight.size() + 1,vector<int>(bagWeight + 1,0));
for(int j = bagWeight; j >= weight[0]; j--)
{
    dp[0][j] = dp[0][j-weight[0]] + value[0];
}

4、确定遍历顺序

有两个遍历维度:物品与背包重量,先遍历物品更好理解。

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]]+values[i])
    }
}

滚动数组优化

1、确定dp数组的定义

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

2、一维dp递推公式

dp[j]可以通过dp[j-weight[i]]推导,其表示容量为j-weight[i]的背包所背的最大价值。

dp[j-weight[i]]+value[i]表示容量为j-物品i重量的背包加上物品i的价值。(即容量为j的背包放入物品i之后的价值)此时dp[j]有两个选择,一个是取自己dp[j],一个是取dp[j-weight[i]]+value[i].

所以递推公式为:

dp[j] = max(dp[j],dp[j-weight[i]]+value[i]);

3、初始化

假设物品价值都是大于0的,dp数组初始化的时候都初始化为0

4、确定遍历顺序

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

二维遍历时,背包容量从小到大,一维遍历,背包容量从大到小。

这是因为倒序遍历是为了保证物品i只被放入一次。二维dp,dp[i][j]是通过上一层dp[i-1][j]计算得到的,所以本层的dp[i][j]并不会产生覆盖。

一维01背包测试代码:

void test_one_dim_01bag()
{
    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[i]; i++)	//遍历物品
    {
        for(int j = bagWeight; j >= weight[i]; j--)	//遍历背包容量
        {
            dp[j] = max(dp[j],dp[j-weight[i]] + value[i]);
        }
    }
    cout << dp[bagWeight] << endl;
}

416. 分割等和子集

求集合里是否出现总和为sum/2的子集。

背包体积为sum/2

背包放入的商品的重量为元素的数值,价值也为元素的数值

背包如何正好被装满,说明找到了总和为sum/2的子集

背包中每个元素都是不可重复放入的。

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

dp[j]表示容量为j的背包,所背物品价值可以最大为dp[j]。

dp[j]表示背包总容量为j,最大可以凑成j的子集总和为dp[j]。

2、确定递推公式

dp[j] = max(dp[j],dp[j-nums[i]] + nums[i]);

3、初始化

vector<int> dp(target+1,0);	//target为背包容量

4、确定遍历顺序

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

AC代码:

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int sum = 0;
        for(int num : nums)
        {
            sum += num;
        }
        if(sum % 2 != 0) return false;
        int target = sum / 2;
        vector<int> dp(target+1,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[j] == target) return true;
            }
        }
        if(dp[target] == target) return true;
        else return false;
    }
};

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

将石头尽量分成两堆相同重量,然后相撞。分成两堆的思路与上一题一致。

class Solution {
public:
    int lastStoneWeightII(vector<int>& stones) {
        int sum = 0;
        for(int stone : stones)
        {
            sum += stone;
        }
        int target = sum / 2;
        //dp[target],容量为target的背包最多能背多重的石头
        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]*2;
    }
};

494. 目标和

所有数字可以分为两堆,一堆符号为正,一堆符号为负。
pos + neg = S 且 pos - neg = sum
所以pos = (S+sum)/2,问题转化为集合nums中找出和为pos的组合。
此时问题就转化为,装满容量为pos背包,有几种方法。
这次和之前遇到的背包问题不一样了,之前都是求容量为j的背包,最多能装多少。
本题则是装满有几种方法。其实这就是一个组合问题了。
1、确定dp数组以及下标
dp[j]表示,填满体积为j的背包,有dp[j]种方法。

2、确定递推公式
不考虑nums[i],填满容量为j-nums[i]的背包,有dp[j-nums[i]]种方法,
如果能搞到nums[i],则填满容量为j-nums[i]的背包,就有dp[j]+dp[j-nums[i]]种方法。

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

3、初始化dp数组
dp[0] = 1,装满容量为0的背包,有1种方法。其他dp[i]均设置为0,在不知道nums[i]的情况下,没有方法。

4、确定遍历顺序
nums外循环,j内循环倒序。

AC代码:

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int sum = 0;
        for(int num : nums)
            sum += num;
        //如果绝对值和比targetabs小,说明都用同一个符号也不能凑成
        if(sum < abs(target)) return 0;
        //如果不能完整的分成两组,那么说明没有方法
        if((target + sum) % 2 == 1) return 0;
        int bagWeight = (target + sum)/2;
        vector<int> dp(bagWeight+1,0);
        dp[0] = 1;
        for(int i = 0; i < nums.size(); i++)
        {
            for(int j = bagWeight; j >= nums[i]; j--)
            {
                dp[j] += dp[j-nums[i]];
            }
        }
        return dp[bagWeight];
    }
};