From 51980ea66372a49453b49f0cf6db9c85b579ad74 Mon Sep 17 00:00:00 2001 From: Sainnhe Park Date: Wed, 8 Feb 2023 17:41:11 +0800 Subject: [PATCH] Add knapsack --- notes/src/SUMMARY.md | 1 + notes/src/knapsack.md | 81 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 notes/src/knapsack.md diff --git a/notes/src/SUMMARY.md b/notes/src/SUMMARY.md index 239ee94..73776a3 100644 --- a/notes/src/SUMMARY.md +++ b/notes/src/SUMMARY.md @@ -55,6 +55,7 @@ - [总结](./dynamic-programming.md) - [基础问题](./dynamic-programming-basic.md) +- [背包问题](./knapsack.md) # STL diff --git a/notes/src/knapsack.md b/notes/src/knapsack.md new file mode 100644 index 0000000..b748d49 --- /dev/null +++ b/notes/src/knapsack.md @@ -0,0 +1,81 @@ +# 背包问题 + +有 `n` 件物品和一个最多能背重量为 `w` 的背包。第 `i` 件物品的重量是 `weight[i]`,得到的价值是 `value[i]`。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。其中 `weight[i]` 和 `value[i]` 都是整数。 + +- `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`:由 `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][0] = 0` + - `dp[0][j]` 当 `j < weight[0]` 时应该为 `0`,否则为 `value[0]` +- 从前往后遍历 + +```cpp +void bag_problem_2d() { + vector weight = {1, 3, 4}; + vector value = {15, 20, 30}; + int w = 4; + + // 二维数组 + vector> dp(weight.size(), vector(bagweight + 1, 0)); + + // 初始化 + for (int j = weight[0]; j <= w; 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]); + } + } +} +``` + +接下来优化我们的代码。 + +注意到递推公式的右侧只用到了 `dp[i - 1]`,我们可以把它看成是 `dp[i]` 上一步的状态,因此每一次迭代的时候我们完全可以将 `dp[i - 1]` 覆盖到 `dp[i]`,这样可以将二维数组压缩到一维。 + +递推公式可以修改成:`dp[j] = max{dp[j], dp[j - weight[i]] + value[i]}` + +这就是滚动数组的思路,使用条件是上一层可以重复利用,直接拷贝到当前层。从递推公式来看,只要递推公式满足了右侧只用了 `dp[i - 1]` 那么就可以压缩。 + +来分析 DP 的思路: + +- `dp[j]` 表示容量为 `j` 的背包所能背的物品的最大价值。 +- 递推公式为 `dp[j] = max{dp[j], dp[j - weight[i]] + value[i]}` +- 初始化 `dp[j] = 0` +- 这次应该从后往前遍历。每次我们访问 `dp[j - weight[i]] + value[i]` 的时候都把物品 `i` 放进去了一次。为了避免重复放进去,应该从后往前遍历。 + +```cpp +void bag_problem_1d() { + vector weight = {1, 3, 4}; + vector value = {15, 20, 30}; + int w = 4; + + // 初始化 + vector dp(w + 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]); + } + } +} +``` + +**Q: 能不能先遍历容量,再遍历物品?** + +**A:** 不行,因为我们本来就是要用上一层的 `i - 1` 来覆盖这一层的 `i`。 + +**Q: 为啥二维不用从后往前呢?** + +**A:** 因为 `dp[i][j]` 都是通过上一层即 `dp[i - 1][j]` 计算而来,本层的 `dp[i][j]` 并不会被覆盖。 + +**Q: 一维从后往前的本质是什么?** + +**A:** 如果从后往前的话,`dp[j - weight[i]] + value[i]` 就用的是上一层的数据(这才是我们想要的),但如果从前往后的话,`dp[j - weight[i]] + value[i]` 就用的是这一层的数据,这将会导致物品被重复放进去。