Add complete knapsack

This commit is contained in:
Sainnhe Park 2023-02-09 16:30:31 +08:00
parent 8551696b52
commit 9a31621c9a

View File

@ -1,6 +1,8 @@
# 背包问题 # 背包问题
`n` 件物品和一个最多能背重量为 `bagWeight` 的背包。 ## 01 背包
`n` 件物品和一个最多能背重量为 `knapsackWeight` 的背包。
`i` 件物品的重量是 `weight[i]`,其价值是 `value[i]`,它们都是正整数。 `i` 件物品的重量是 `weight[i]`,其价值是 `value[i]`,它们都是正整数。
@ -20,23 +22,23 @@
- `i``j` 都从前往后遍历 - `i``j` 都从前往后遍历
```cpp ```cpp
void bag_problem_2d() { void knapsack_problem_2d() {
vector<int> weight = {1, 3, 4}; vector<int> weight = {1, 3, 4};
vector<int> value = {15, 20, 30}; vector<int> value = {15, 20, 30};
int bagWeight = 4; int knapsackWeight = 4;
// 二维数组 // 二维数组
// 之所以初始化 j 的范围是 0 ~ bagWeight + 1 ,是因为我们会索引 dp[i][bagWeight] // 之所以初始化 j 的范围是 0 ~ knapsackWeight + 1 ,是因为我们会索引 dp[i][knapsackWeight]
vector<vector<int>> dp(weight.size(), vector<int>(bagWeight + 1, 0)); vector<vector<int>> dp(weight.size(), vector<int>(knapsackWeight + 1, 0));
// 初始化 // 初始化
for (int j = weight[0]; j <= bagWeight; j++) { for (int j = weight[0]; j <= knapsackWeight; j++) {
dp[0][j] = value[0]; dp[0][j] = value[0];
} }
// 开始遍历 // 开始遍历
for (int i = 1; i < weight.size(); i++) { // 遍历物品 for (int i = 1; i < weight.size(); i++) { // 遍历物品
for (int j = 1; j <= bagWeight; j++) { // 遍历背包容量 for (int j = 1; j <= knapsackWeight; j++) { // 遍历背包容量
if (j < weight[i]) if (j < weight[i])
dp[i][j] = dp[i - 1][j]; dp[i][j] = dp[i - 1][j];
else else
@ -60,7 +62,7 @@ void bag_problem_2d() {
- 递推公式: - 递推公式:
- `dp[j] = dp[j]`, `if j < weight[i]` 因为如果现在的物品重量比背包容量还大,那背包就装不下了,只能不装现在的这一个,那就是 `dp[i][j] = dp[i - 1][j]`,也就是 `dp[j] = dp[j]` - `dp[j] = dp[j]`, `if j < weight[i]` 因为如果现在的物品重量比背包容量还大,那背包就装不下了,只能不装现在的这一个,那就是 `dp[i][j] = dp[i - 1][j]`,也就是 `dp[j] = dp[j]`
- `dp[j] = max{dp[j], dp[j - weight[i]] + value[i]}`, `if j >= weight[i]` 这是能装下的情况 - `dp[j] = max{dp[j], dp[j - weight[i]] + value[i]}`, `if j >= weight[i]` 这是能装下的情况
- 总结一下就是只有当 `j >= weight[i]` 的时候我们才会调用第二个递推公式,否则 `dp[j]` 不变,于是我们可以将这一个放到第二层 for 循环里,第二层 for 循环的遍历范围是 `weight[i] <= j <= bagWeight` - 总结一下就是只有当 `j >= weight[i]` 的时候我们才会调用第二个递推公式,否则 `dp[j]` 不变,于是我们可以将这一个放到第二层 for 循环里,第二层 for 循环的遍历范围是 `weight[i] <= j <= knapsackWeight`
- 由于滚动数组每次都会覆盖上一层,因此初始化的时候我们只需要将滚动数组作为二维数组的第一层初始化 - 由于滚动数组每次都会覆盖上一层,因此初始化的时候我们只需要将滚动数组作为二维数组的第一层初始化
- `dp[j]``j >= weight[0]` 时应该为 `value[0]` 也就是能装下第 `0` 个物品,否则为 `0` - `dp[j]``j >= weight[0]` 时应该为 `value[0]` 也就是能装下第 `0` 个物品,否则为 `0`
- 如果初始化了 `i = 0` 的第一层的话,那么双重 for 循环就应该从 `1` 开始。 - 如果初始化了 `i = 0` 的第一层的话,那么双重 for 循环就应该从 `1` 开始。
@ -70,16 +72,16 @@ void bag_problem_2d() {
- 但是背包重量 `j` 应该从后往前遍历 - 但是背包重量 `j` 应该从后往前遍历
```cpp ```cpp
void bag_problem_1d() { void knapsack_problem_1d() {
vector<int> weight = {1, 3, 4}; vector<int> weight = {1, 3, 4};
vector<int> value = {15, 20, 30}; vector<int> value = {15, 20, 30};
int bagWeight = 4; int knapsackWeight = 4;
// 初始化 // 初始化
// 之所以初始化 j 的范围是 0 ~ bagWeight + 1 ,是因为我们会索引 dp[bagWeight] // 之所以初始化 j 的范围是 0 ~ knapsackWeight + 1 ,是因为我们会索引 dp[knapsackWeight]
vector<int> dp(bagWeight + 1, 0); vector<int> dp(knapsackWeight + 1, 0);
for (int i = 0; i < weight.size(); i++) { // 遍历物品 for (int i = 0; i < weight.size(); i++) { // 遍历物品
for (int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量 for (int j = knapsackWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
} }
} }
@ -98,7 +100,7 @@ void bag_problem_1d() {
**A:** 如果从后往前的话,`dp[j - weight[i]] + value[i]` 就用的是上一层的数据(这才是我们想要的),但如果从前往后的话,`dp[j - weight[i]] + value[i]` 就用的是这一层的数据,这将会导致物品被重复放进去。 **A:** 如果从后往前的话,`dp[j - weight[i]] + value[i]` 就用的是上一层的数据(这才是我们想要的),但如果从前往后的话,`dp[j - weight[i]] + value[i]` 就用的是这一层的数据,这将会导致物品被重复放进去。
## [416. 分割等和子集](https://leetcode.cn/problems/partition-equal-subset-sum/) ### [416. 分割等和子集](https://leetcode.cn/problems/partition-equal-subset-sum/)
二维数组: 二维数组:
@ -127,7 +129,9 @@ void bag_problem_1d() {
本题中可以不初始化第一层,然后 `i` 从 0 开始。 本题中可以不初始化第一层,然后 `i` 从 0 开始。
## [1049. 最后一块石头的重量 II](https://leetcode.cn/problems/last-stone-weight-ii/) 考虑需不需要初始化第一层其实很简单,就看如果不初始化的话,第一层循环的逻辑是否正确。
### [1049. 最后一块石头的重量 II](https://leetcode.cn/problems/last-stone-weight-ii/)
仔细思考一下每个石头重量的加减方式,你会发现其实最终的重量可以这样表示: 仔细思考一下每个石头重量的加减方式,你会发现其实最终的重量可以这样表示:
@ -159,3 +163,26 @@ void bag_problem_1d() {
- `j``total/2` 向下取整,遍历到 `stones[i]` - `j``total/2` 向下取整,遍历到 `stones[i]`
本题中可以不初始化第一层,然后 `i` 从 0 开始。 本题中可以不初始化第一层,然后 `i` 从 0 开始。
考虑需不需要初始化第一层其实很简单,就看如果不初始化的话,第一层循环的逻辑是否正确。
## 完全背包
和 01 背包的区别就在于01 背包的每个元素只能用一次,而完全背包的每个物品能够重复使用。
代码也很简单,我们知道 01 背包的第二层 for 循环是从大到小遍历,这是为了去重,而完全背包是可以重复添加物品的,因此要从小到大遍历
```cpp
void complete_knapsack_problem_1d() {
vector<int> weight = {1, 3, 4};
vector<int> value = {15, 20, 30};
int knapsackWeight = 4;
vector<int> dp(knapsackWeight + 1, 0);
for (int i = 0; i < weight.size(); i++) { // 遍历物品
for (int j = weight[i]; j <= knapsackWeight; j++) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
}
```