问题

给你一个可放总重量为 W 的背包和 N 个物品,对每个物品,有重量 w 和价值 v 两个属性,那么第 i 个物品的重量为 w[i],价值为 v[i]。现在让你用这个背包装物品,每种物品都可以选择任意多个,问这个背包最多能装的价值是多少?

  1. 示例:
  2. 输入:W = 5, N = 3
  3. w = [3, 2, 1], v = [5, 2, 3]
  4. 输出:15
  5. 解释:当 i = 2 时,选取 5 次,总价值为 5 * 3 = 15

算法问题分析

不同于 0-1 背包问题(每件物品只能拿一次),在完全背包问题中,每件物品可以拿任意多件,只要背包装得下就行。

如果从每件物品的角度来看,与之相关的决策已经不再是选拿(1)或者不拿(0)了;而是拿 0 件、拿 1 件、拿 2 件……直到拿到 (W/w[i]) 件物品为止。

状态转移方程

image.png
代码实现java版

  1. public static void main(String[] args) {
  2. int N = 3, W = 5; // 物品的总数,背包能容纳的总重量
  3. int[] w = {0, 3, 2, 1}; // 物品的重量
  4. int[] v = {0, 5, 2, 3}; // 物品的价值
  5. int dp = dp(w, v, N, W);
  6. System.out.println(dp);
  7. }
  8. /**
  9. * @param w 重量数组
  10. * @param v 价值数组
  11. * @param N 有多少个可选物品
  12. * @param W 背包容量
  13. */
  14. public static int dp(int[] w, int[] v, int N, int W) {
  15. // 创建备忘录
  16. int[][] dp = new int[N + 1][W + 1];
  17. //
  18. for (int tn = 1; tn < N + 1; tn++) {
  19. for (int rw = 1; rw < W + 1; rw++) {
  20. //
  21. if (rw < w[tn]) {
  22. // 不选
  23. dp[tn][rw] = dp[tn - 1][rw];
  24. } else {
  25. int maxNum = rw / w[tn];
  26. for (int k = 0; k <= maxNum; k++) {
  27. // 前n-1种物品的剩余可装量
  28. int otherW = rw - k * w[tn];
  29. dp[tn][rw] = Math.max(dp[tn][rw], dp[tn - 1][otherW] + k * v[tn]);
  30. }
  31. }
  32. }
  33. }
  34. return dp[N][W];
  35. }

输出

15

状态转移过程

image.png

时间复杂度优化

如果我们认真分析上面的代码,就可以发现代码中使用了三重循环:

  1. 首先是遍历物品;
  2. 然后是遍历剩余容量;
  3. 最后是遍历物品数量。

那么这个解法的算法时间复杂度是多少呢?如果我们假定物品数量是 k,容量是 v,那么最后的时间复杂度就是 O(kv²)。

改进代码的时间复杂度

我们查看上一份代码不难看出,对于第tn个物品,其rw=0,1,2,3….W,前每个列都要遍历多遍,这属于重复子问题,我们就可以将其转为0-1背包问题,不管前面已经使用了tn这个物品几次,只关心当前容量这一次是否需要放进去即可。

  1. /**
  2. * @param w 重量数组
  3. * @param v 价值数组
  4. * @param N 有多少个可选物品
  5. * @param W 背包容量
  6. */
  7. public static int dp(int[] w, int[] v, int N, int W) {
  8. // 创建备忘录
  9. int[][] dp = new int[N + 1][W + 1];
  10. //
  11. for (int tn = 1; tn < N + 1; tn++) {
  12. for (int rw = 1; rw < W + 1; rw++) {
  13. //
  14. if (rw < w[tn]) {
  15. // 不选
  16. dp[tn][rw] = dp[tn - 1][rw];
  17. } else {
  18. dp[tn][rw] = Math.max(dp[tn][rw], dp[tn][rw - w[tn]] + v[tn]);
  19. }
  20. }
  21. }
  22. return dp[N][W];
  23. }

其得到的状态转移过程一致