leetcode/notes/src/knapsack.md
Sainnhe Park 51980ea663
All checks were successful
ci/woodpecker/push/test Pipeline was successful
Add knapsack
2023-02-08 17:41:11 +08:00

3.6 KiB

背包问题

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]
  • 从前往后遍历
void bag_problem_2d() {
  vector<int> weight = {1, 3, 4};
  vector<int> value = {15, 20, 30};
  int w = 4;

  // 二维数组
  vector<vector<int>> dp(weight.size(), vector<int>(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 放进去了一次。为了避免重复放进去,应该从后往前遍历。
void bag_problem_1d() {
  vector<int> weight = {1, 3, 4};
  vector<int> value = {15, 20, 30};
  int w = 4;

  // 初始化
  vector<int> 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] 就用的是这一层的数据,这将会导致物品被重复放进去。