背包理论基础
有一点要先说明:leetcode上没有纯01背包的问题,都是01背包应用方面的题目,也就是需要转化为01背包问题。
0-1背包
有n件物品和一个最多能背重量w的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
例:
背包最大重量为4,
物品重量为:
重量 | 价值 | |
---|---|---|
物品0 | 1 | 15 |
物品1 | 3 | 20 |
物品2 | 4 | 30 |
求背包能背起的物品最大价值是多少?
0-1背包问题的思路就是一个一个物品去尝试,一点一点扩大能够容纳的体积大小。
二维dp数组解01背包
1.确定dp数组以及下标的含义
dp[i][j]表示从下标为[0-i]的物品里选取,放进容量为j的背包
2.确定递推公式
- 不放物品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][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
推导0-1背包问题的状态转移方程的过程很多时候就是在讨论当前考虑的物品选与不选,选了计算一个值,不选再计算一个值,然后综合
3.dp数组初始化
如果背包容量j为0,即dp[i][0],那么背包价值总和一定为0;
如果i为0,即dp[0][j],存放编号0的物品,当 j < weight[0]的时候,dp[0][j] 应该是0,因为背包容量比编号0的物品重量还小。当j >= weight[0]时,dp[0][j] 应该是value[0],因为背包容量放足够放编号0物品。
由递推公式dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])可以看出dp[i][j] 是由左上方和正上方数值(dp[i-1][j]和dp[i - 1][j - weight[i]])推导出,那么其他下标可以初始为任何值。
4.确定遍历顺序
有两个遍历维度:物品和背包重量,怎么遍历都不影响dp[i][j]的推导
先遍历物品,再遍历背包重量:
// 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]);
}
}
先遍历背包,再遍历物品:
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数组(滚动数组)解01背包
1.确定dp数组以及下标的含义
在一维dp数组中,dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]。
2.一维dp数组的递推公式
- dp[j]可以通过dp[j - weight[i]]推导出来,dp[j - weight[i]]表示容量为j - weight[i]的背包所背的最大价值;
- dp[j - weight[i]] + value[i] 表示 容量为 j - 物品i重量 的背包加上物品i的价值;
- 此时dp[j]有两个选择,一个是dp[j]不变,相当于 二维dp数组中的dp[i-1][j],即不放物品i,一个是取dp[j - weight[i]] + value[i],即放物品i,然后去两者中大的。
所以递推公式为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
3.一维dp数组初始化
dp[0]=0,即容量为0时价值为0;
假设物品价值都为正值,因为dp要取最大值,那么其他其他下标初始化为0即可。
这样才能让dp数组在递归公式的过程中取的最大的价值,而不是被初始值覆盖了。
4.一维dp数组遍历顺序
如果使用一维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历
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遍历的时候,背包容量是从小到大,而一维dp遍历的时候,背包是从大到小。
倒序遍历是为了保证物品i只被放入一次
5.举例推到dp数组
题目1:416. 分割等和子集
可以将问题转换成:是否可以从输入数组中挑选出一些正整数,使得这些数的和 等于 整个数组元素的和的一半(剩下没有被挑选的正整数的和也是整个数组元素和的一半)。
本题和0-1背包问题的不同:
0-1背包问题选取的物品的总重量 不能超过 背包的总容量;
本题选取的数字之和 等于 数组所有元素和的一半。
0-1背包问题的思路就是一个一个物品去尝试,一点一点扩大能够容纳的体积大小。
本题思路就是从一个数组nums中拿出一部分元素,求出这一部分元素的和,是否等于所有元素和的一半。
二维dp数组
1.确定dp数组以及下标的含义
dp[i][j]表示从数组下标[0,i] 取数,使 这些数的和j等于所有元素和的一半。
2.确定状态转移方程
- 不选nums[i]:如果不选下标为i的元素,那么dp[i][j]就取决于dp[i-1][j],dp[i][j]=dp[i-1][j];
- 选择nums[i]:
///①如果当前下标为i的元素值nums[i]等于所以有元素和的一半,即nums[i]=j,那么可以把nums[i]当作一个子集,这里相当于没选nums[i],因为它都等于j了,放不下,此时dp[i][j]=dp[i-1][j]
///②如果nums[i]<j(即nums[i]被装进背包),那么背包剩余容量为j-nums[i],此时的dp[i][j] = dp[i-1][j-nums[i]]
状态转移方程为:
3.数组初始化
如果j=0,即不选取任何正整数(背包容量为0),那么不论选取任何书都装满背包,即dp[i][0]=true;(这个解释感觉有点牵强)
在表格中的第一行,找有没有能塞满背包的数,即if (nums[0] <= target) {
dp[0][nums[0]] = true;}
输出为dp[n-1][sum/2],n-1为数组的长度,表示是否能从大小为n-1的数组中选取一些元素,使其和为sum/2
4.确定遍历顺序
先遍历物品,再遍历背包重量(更好理解吧)
for (int i = 1; i < nums.size(); ++i) { //i从1开始,因为有时会用到i - 1,为了防止数组越界
for (int j = 1; j <= bagwight; ++j) {
if (j >= nums[i]) dp[i][j] = dp[i - 1][j - nums[i]] || dp[i - 1][j]; //选择nums[i]
else dp[i][j] = dp[i - 1][j]; //不选nums[i]
}
}
5.举例推导dp数组
以nums=[1,5,11,5]为例,初始化后的dp数组:
推导后的dp数组:
二维数组始终理解的不是很清晰,评论区大佬也是各有各的说法
class Solution {
public:
bool canPartition(vector<int>& nums) {
if (nums.size() < 2) return false; //若nums大小小于2则肯定不能
int sum = accumulate(nums.begin(), nums.end(), 0); //数组所有元素和
int bagweight = sum / 2; //背包容量
if (sum % 2 == 1) return false; //和不能为奇数,否则不可能将数组分割成元素相等的两个子集
vector<vector<int>> dp(nums.size(), vector<int>(bagweight + 1, 0)); //定义dp数组 并初始化数组全部为0
for (int i = 0; i < nums.size(); i++) dp[i][0] = true; //容量为0时,装什么都满
if (nums[0] <= bagweight) dp[0][nums[0]] = true; //二维数组中第一行是否有能塞满背包的数
for (int i = 1; i < nums.size(); i++) { //i从1开始,因为有时会用到i - 1,为了防止数组越界
for (int j = 1; j <= bagweight; j++) {
if (j >= nums[i]) dp[i][j] = dp[i - 1][j - nums[i]] || dp[i - 1][j]; //容量够,可以选择装(dp[i - 1][j - nums[i]])或不装(dp[i - 1][j])
else dp[i][j] = dp[i - 1][j]; //不选nums[i],j<nums[i],装不下了
}
}
return dp[nums.size() - 1][bagweight];
}
};
一维dp数组(滚动数组)
1.确定dp数组以及下标的含义
01背包中,dp[j] 表示: 容量为j的背包,所背的物品价值可以最大为dp[j]。
本题中,dp[j]表示 背包总容量是j,最大可以凑成j的子集总和为dp[j]。
2.确定状态转移方程
01背包的递推公式为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
本题中放入背包的是数值nums[i] (相当于01背包中的重量),价值也是nums[i]
- dp[j]可以通过dp[j - nums[i]]推导出来,dp[j - nums[i]]表示容量为j - nums[i]的背包中的元素和;
- dp[j - nums[i]] + nums[i] 表示 容量为j的背包减去nums[i]的重量加上nums[i]的价值;
- 此时dp[j]有两个选择,一个是取自己dp[j],相当于 二维dp数组中的dp[i-1][j],即不取nums[i],一个是取dp[j - nums[i]] + nums[i],即选nums[i],然后去两者中大的。
因此状态转移公式为:dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
3.dp数组初始化
dp[0]=0,即容量为0时价值为0;
假设物品价值都为正值,因为dp要取最大值,那么其他其他下标初始化为0即可。
4.确定遍历顺序
使用一维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历
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]);
}
}
5.举例推导dp数组
初始化后:
推导后:
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = accumulate(nums.begin(), nums.end(), 0); //数组所有元素和
if (sum % 2 == 1) return false; //和不能为奇数,否则不可能将数组分割成元素相等的两个子集
int bagweight = sum / 2; //背包容量
vector<int> dp(bagweight + 1, 0); //定义dp数组 并初始化数组全部为0
for (int i = 0; i < nums.size(); i++) {
for (int j = bagweight; j >= nums[i]; j--) { //每一个元素一定是不可重复放入,所以从大到小遍历
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
}
}
if (dp[bagweight] == bagweight) return true; //dp[j]=(sum/2)
return false;
}
};
一维数组比二维数组简单多了!!!!!!!!!!!!
题目2:1049. 最后一块石头的重量 II
这题刚开始看并没有感觉是01背包问题,直到代码随想录的一句话:让石头分成两堆,相撞之后剩下的石头最小
找到容量为一半时可以装入的石头,和剩余的一半石头容量比较,既可以得到题目所求剩余石头重量。
那其实就和416题差不多了
一维dp数组
1.确定dp数组以及下标的含义
dp[j]表示:重量为j的背包,放入石头的最大重量为dp[j]。
2.一维dp数组的递推公式
本题物品的重量为weight[i],价值也为weight[i]
- dp[j]可以通过dp[j - weight[i]]推导出来,dp[j - weight[i]]表示为j - weight[i]的背包所背的最大重量;
- dp[j - weight[i]] + weight[i] 表示 容量为 j - 物品i重量 的背包加上物品i的重量;
- 此时dp[j]有两个选择,一个是dp[j]不变,相当于 二维dp数组中的dp[i-1][j],即不放物品i,一个是取dp[j - weight[i]] + weight[i],即放物品i,然后去两者中大的。
所以递推公式为:dp[j] = max(dp[j], dp[j - weight[i]] + weight[i]);
3.一维dp数组初始化
dp[0]=0,即容量为0时重量为0;
假设物品价值都为正值,因为dp要取最大值,那么其他其他下标初始化为0即可。
class Solution {
public:
int lastStoneWeightII(vector<int>& stones) {
int sum = accumulate(stones.begin(), stones.end(), 0);
int bagweight = sum / 2;
vector<int> dp(bagweight + 1, 0);
for (int i = 0; i < stones.size(); i++) {
for (int j = bagweight; j >= stones[i]; j--) {
dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
}
}
return abs(dp[bagweight] - (sum - dp[bagweight])); //两堆石头相撞
}
};
题目3:494. 目标和
数组的元素和为sum,添加+号的元素和为x,添加-号的元素和为sum-x,那么就能得到x-(sum-x)=2x-sum=target,化简后x=(sum+target)/2
一维dp数组
问题转化成在数组nums中选取若干元素,使得这些元素之和等于x,变成了01背包问题。
但是和之前不同的是,本题求的是装满容量为x的背包有几种方法
1.确定dp数组以及下标的含义
dp[j] 表示:填满j(包括j)这么大容积的包,有dp[j]种方法
2.一维dp数组的递推公式
这一题的递推公式有些不好理解:
要填满容量为j的背包,有dp[j]种方法;
那么填满容量为j-nums[i],就有dp[j-nums[i]]种方法;
如果有nums[i],就可以用dp[j-nums[i]]种方法凑成dp[j]
所以递推公式为:dp[j] = max(dp[j], dp[j - weight[i]] + weight[i]);
3.一维dp数组初始化
dp[0]=1,即容量为0时不选数;
假设物品价值都为正值,因为dp要取最大值,那么其他其他下标初始化为0即可。
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int sum = accumulate(nums.begin(), nums.end(), 0);
int bagweight = (sum + target) / 2;
if ((sum + target) % 2 == 1 && abs(target) > sum) return 0;
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[i - nums[i]]; //求装满背包有几种方法的一半都是这个公式
}
}
return dp[bagweight];
}
};
题目4:474.一和零
摆烂了不写了