背包问题
背包问题是一类经典的组合优化问题,基本形式是:给定一个固定大小的背包,和一些物品,每个物品具有自己的体积和价值,在不超过背包容量限制的前提下,将若干个物品装入背包,使得装入的物品总价值最大。
问题类型 | 是否能选取多次 | 物品是否分组 |
---|---|---|
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;
}
};