背包问题-Java版实现

88 阅读6分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

背包问题

简而言之:背包承重有限情况如果装入物品的价值最高。细化分类有有三种

  1. 01背包问题(unbounded knapsack problem):一共有N件物品,第i(i从1开始)件物品的重量为w[i],价值为v[i]。在总重量不超过背包承载上限W的情况下,能够装入背包的最大价值是多少
  2. 完全背包问题(unbounded knapsack problem):与01背包不同就是每种物品可以有无限多个:一共有N种物品,每种物品有无限多个,第i(i从1开始)种物品的重量为w[i],价值为v[i]。在总重量不超过背包承载上限W的情况下,能够装入背包的最大价值是多少
  3. 多重背包问题(bounded knapsack problem):与前面不同就是每种物品是有限个:一共有N种物品,第i(i从1开始)种物品的数量为n[i],重量为w[i],价值为v[i]。在总重量不超过背包承载上限W的情况下,能够装入背包的最大价值是多少?

解决方案

穷举法

穷举法几乎是所有问题的答案,缺点是时间复杂度高,对应01背包问题的时间复杂度已经高达幂数级别

代码

package org.gallant.leetcode.algorithms.domain;

/**
 * 背包问题
 *
 * @author : 会灰翔的灰机
 * @date : 2021/8/28
 */
public class KnapsackProblem {

  public static void main(String[] args) {
    int[] weights = new int[]{3, 1, 5, 7, 8};
    int[] values = new int[] {5, 3, 7, 9, 10};
    exhaustivity(weights, values);
  }

  private static void exhaustivity(int[] weights, int[] values) {
    // 01背包问题,每件物品均有两种选择,放入背包,或不放入背包
    int size = 2;
    for (int i = 0; i < size; i++) {
      for (int j = 0; j < size; j++) {
        for (int k = 0; k < size; k++) {
          for (int l = 0; l < size; l++) {
            for (int m = 0; m < size; m++) {
              int summaryWeight = 0, summaryValue =0;
              summaryWeight += i * weights[0];
              summaryValue += i * values[0];
              summaryWeight += j * weights[1];
              summaryValue += j * values[1];
              summaryWeight += k * weights[2];
              summaryValue += k * values[2];
              summaryWeight += l * weights[3];
              summaryValue += l * values[3];
              summaryWeight += m * weights[4];
              summaryValue += m * values[4];
              System.out.printf("i:%s,j:%s,k:%s,l:%s,m:%s,summaryWeight:%s,summaryValue:%s\n", i, j, k, l, m, summaryWeight, summaryValue);
            }
          }
        }
      }
    }
  }
}

结果

从结果中筛选符合需求的,比如:背包总承重为15,最大价值为多少? 案例数据: int[] weights = new int[]{3, 1, 5, 7, 8}; int[] values = new int[] {5, 3, 7, 9, 10}; 答案是:21,对应物品组合顺序为:i:1,j:0,k:1,l:1,m:0 重量为3,5,7的物品

i:0,j:0,k:0,l:0,m:0,summaryWeight:0,summaryValue:0
i:0,j:0,k:0,l:0,m:1,summaryWeight:8,summaryValue:10
i:0,j:0,k:0,l:1,m:0,summaryWeight:7,summaryValue:9
i:0,j:0,k:0,l:1,m:1,summaryWeight:15,summaryValue:19
i:0,j:0,k:1,l:0,m:0,summaryWeight:5,summaryValue:7
i:0,j:0,k:1,l:0,m:1,summaryWeight:13,summaryValue:17
i:0,j:0,k:1,l:1,m:0,summaryWeight:12,summaryValue:16
i:0,j:0,k:1,l:1,m:1,summaryWeight:20,summaryValue:26
i:0,j:1,k:0,l:0,m:0,summaryWeight:1,summaryValue:3
i:0,j:1,k:0,l:0,m:1,summaryWeight:9,summaryValue:13
i:0,j:1,k:0,l:1,m:0,summaryWeight:8,summaryValue:12
i:0,j:1,k:0,l:1,m:1,summaryWeight:16,summaryValue:22
i:0,j:1,k:1,l:0,m:0,summaryWeight:6,summaryValue:10
i:0,j:1,k:1,l:0,m:1,summaryWeight:14,summaryValue:20
i:0,j:1,k:1,l:1,m:0,summaryWeight:13,summaryValue:19
i:0,j:1,k:1,l:1,m:1,summaryWeight:21,summaryValue:29
i:1,j:0,k:0,l:0,m:0,summaryWeight:3,summaryValue:5
i:1,j:0,k:0,l:0,m:1,summaryWeight:11,summaryValue:15
i:1,j:0,k:0,l:1,m:0,summaryWeight:10,summaryValue:14
i:1,j:0,k:0,l:1,m:1,summaryWeight:18,summaryValue:24
i:1,j:0,k:1,l:0,m:0,summaryWeight:8,summaryValue:12
i:1,j:0,k:1,l:0,m:1,summaryWeight:16,summaryValue:22
i:1,j:0,k:1,l:1,m:0,summaryWeight:15,summaryValue:21
i:1,j:0,k:1,l:1,m:1,summaryWeight:23,summaryValue:31
i:1,j:1,k:0,l:0,m:0,summaryWeight:4,summaryValue:8
i:1,j:1,k:0,l:0,m:1,summaryWeight:12,summaryValue:18
i:1,j:1,k:0,l:1,m:0,summaryWeight:11,summaryValue:17
i:1,j:1,k:0,l:1,m:1,summaryWeight:19,summaryValue:27
i:1,j:1,k:1,l:0,m:0,summaryWeight:9,summaryValue:15
i:1,j:1,k:1,l:0,m:1,summaryWeight:17,summaryValue:25
i:1,j:1,k:1,l:1,m:0,summaryWeight:16,summaryValue:24
i:1,j:1,k:1,l:1,m:1,summaryWeight:24,summaryValue:34

动态规划

问题拆解,背包装满最大价值可以拆解为,数量设为:n,背包承重为:W,物品重量:wi,物品组合最大价值:maxV(n),当前物品价值:vi ​

那么最大价值为max(包含当前物品,不包含当前物品),转换为公式为:

  1. 包含当前物品:maxV(n-1, W - wi) + vi
  2. 不包含当前物品:maxV(n-1, W)

那么最大价值则为二者其中之一,即我们需要取二者的最大值即可: maxV(n)= max(maxV(n-1,W - wi),maxV(n-1,W))

代码

  public static void main(String[] args) {
    // 案例1
    int[] weights = new int[]{3, 1, 5, 7, 8};
    int[] values = new int[] {5, 3, 7, 9, 10};
//    exhaustivity(weights, values);
    // 遍历每个物品,计算包含物品与不包含物品的最大价值
    // 根据输出结果选择最大价值组合
    /*为什么需要遍历所有物品(3,1,5,7,8)计算最大价值?

    dynamicPrograming4KnapsackProblem代码实现每次仅判断了是否包含物品3的场景,不包含是否包含其他物品的场景。
    假设仅遍历一边物品3,那么得到的结论是:最大价值包含物品3,或不包含。但是对于物品1没有判断是否包含,物品3包含或不包含两个代码分支可能都包含物品1,因此需要遍历计算所有物品,然后选择大价值价值组合*/
    for (int i = 0; i < weights.length; i++) {
      if (i != 0) {
        exchange(weights, values, i, 0);
      }
      PrintUtil.printArray(weights);
      PrintUtil.printArray(values);
      dynamicPrograming4KnapsackProblem(weights, values, 0, 15, 15, 0, 0, new ArrayList<>());
      System.out.println("----------------");
    }
    System.out.println("######################案例2######################");
    // 案例2
    weights = new int[]{3, 1, 5, 7, 8};
    values = new int[] {5, 3, 7, 9, 10};
    for (int i = 0; i < weights.length; i++) {
      if (i != 0) {
        exchange(weights, values, i, 0);
      }
      PrintUtil.printArray(weights);
      PrintUtil.printArray(values);
      dynamicPrograming4KnapsackProblem(weights, values, 0, 12, 12, 0, 0, new ArrayList<>());
      System.out.println("----------------");
    }
  }

  /**
   * 计算最大价值中是否包含索引0处的物品
   *
   * @param weights 所有物品对应的重量
   * @param values 所有物品对应的价值
   * @param index 当前遍历索引
   * @param finalThreshold  最大承重阈值
   * @param threshold 当前剩余承重阈值
   * @param summaryWeight 总重量
   * @param summaryValue  总价值
   * @param indexes 遍历过的索引列表
   * @return : 最大价值
   */
  private static int dynamicPrograming4KnapsackProblem(int[] weights, int[] values, int index, int finalThreshold, int threshold, int summaryWeight, int summaryValue, List<Integer> indexes) {
    if (index >= weights.length) {
      return summaryValue;
    }
    int w = weights[index];
    int v = values[index];
    // 达到背包最大承重,结束遍历
    if (summaryWeight + w > finalThreshold) {
      System.out.printf("sw:%s,sv:%s,%s%n", summaryWeight, summaryValue, indexes);
      indexes.clear();
      return summaryValue;
    }
    List<Integer> indexesOld = new ArrayList<>(indexes);
    indexes.add(index);
    // 包含当前物品的最大价值
    int maxValue1 = dynamicPrograming4KnapsackProblem(weights, values, index + 1, finalThreshold, threshold - w, summaryWeight + w, summaryValue + v, indexes);
    // 不包含当前物品的最大价值
    int maxValue2 = dynamicPrograming4KnapsackProblem(weights, values, index + 1, finalThreshold, threshold, summaryWeight, summaryValue, indexesOld);
    if (maxValue1 > maxValue2) {
      return maxValue1;
    } else {
      return maxValue2;
    }
  }

  private static void exchange(int[] weights, int[] values, int i, int j) {
    int hw = weights[j];
    int hv = values[j];
    weights[j] = weights[i];
    values[j] = values[i];
    weights[i] = hw;
    values[i] = hv;
  }

结果

案例1结果

3,1,5,7,8, 5,3,7,9,10, 总重量:15,总价值:21,物品:3,5,7 sw:15,sv:21,[0, 2, 3] ​

案例2结果

8,3,1,5,7, 10,5,3,7,9, 总重量:12,总价值:18,物品:8,3,1 sw:12,sv:18,[0, 1, 2]

// 包含物品3或不包含物品3的最大价值
3,1,5,7,8,
5,3,7,9,10,
sw:9,sv:15,[0, 1, 2]
sw:11,sv:17,[0, 1, 3]
sw:15,sv:21,[0, 2, 3]
sw:8,sv:12,[0, 2]
sw:10,sv:14,[0, 3]
sw:13,sv:19,[1, 2, 3]
sw:8,sv:12,[1, 3]
sw:12,sv:16,[2, 3]
----------------
// 包含物品1或不包含物品1的最大价值
1,3,5,7,8,
3,5,7,9,10,
sw:9,sv:15,[0, 1, 2]
sw:11,sv:17,[0, 1, 3]
sw:13,sv:19,[0, 2, 3]
sw:8,sv:12,[0, 3]
sw:15,sv:21,[1, 2, 3]
sw:8,sv:12,[1, 2]
sw:10,sv:14,[1, 3]
sw:12,sv:16,[2, 3]
----------------
5,3,1,7,8,
7,5,3,9,10,
sw:9,sv:15,[0, 1, 2]
sw:15,sv:21,[0, 1, 3]
sw:8,sv:12,[0, 1]
sw:13,sv:19,[0, 2, 3]
sw:12,sv:16,[0, 3]
sw:11,sv:17,[1, 2, 3]
sw:10,sv:14,[1, 3]
sw:8,sv:12,[2, 3]
----------------
7,3,1,5,8,
9,5,3,7,10,
sw:11,sv:17,[0, 1, 2]
sw:15,sv:21,[0, 1, 3]
sw:10,sv:14,[0, 1]
sw:13,sv:19,[0, 2, 3]
sw:8,sv:12,[0, 2]
sw:12,sv:16,[0, 3]
sw:9,sv:15,[1, 2, 3]
sw:8,sv:12,[1, 3]
----------------
8,3,1,5,7,
10,5,3,7,9,
sw:12,sv:18,[0, 1, 2]
sw:11,sv:15,[0, 1]
sw:14,sv:20,[0, 2, 3]
sw:9,sv:13,[0, 2]
sw:13,sv:17,[0, 3]
sw:9,sv:15,[1, 2, 3]
----------------
######################案例2######################
3,1,5,7,8,
5,3,7,9,10,
sw:9,sv:15,[0, 1, 2]
sw:11,sv:17,[0, 1, 3]
sw:8,sv:12,[0, 2]
sw:10,sv:14,[0, 3]
sw:6,sv:10,[1, 2]
sw:8,sv:12,[1, 3]
sw:12,sv:16,[2, 3]
sw:5,sv:7,[2]
sw:7,sv:9,[3]
----------------
1,3,5,7,8,
3,5,7,9,10,
sw:9,sv:15,[0, 1, 2]
sw:11,sv:17,[0, 1, 3]
sw:6,sv:10,[0, 2]
sw:8,sv:12,[0, 3]
sw:8,sv:12,[1, 2]
sw:10,sv:14,[1, 3]
sw:12,sv:16,[2, 3]
sw:5,sv:7,[2]
sw:7,sv:9,[3]
----------------
5,3,1,7,8,
7,5,3,9,10,
sw:9,sv:15,[0, 1, 2]
sw:8,sv:12,[0, 1]
sw:6,sv:10,[0, 2]
sw:12,sv:16,[0, 3]
sw:5,sv:7,[0]
sw:11,sv:17,[1, 2, 3]
sw:10,sv:14,[1, 3]
sw:8,sv:12,[2, 3]
sw:7,sv:9,[3]
----------------
7,3,1,5,8,
9,5,3,7,10,
sw:11,sv:17,[0, 1, 2]
sw:10,sv:14,[0, 1]
sw:8,sv:12,[0, 2]
sw:12,sv:16,[0, 3]
sw:7,sv:9,[0]
sw:9,sv:15,[1, 2, 3]
sw:8,sv:12,[1, 3]
sw:6,sv:10,[2, 3]
sw:5,sv:7,[3]
----------------
8,3,1,5,7,
10,5,3,7,9,
sw:12,sv:18,[0, 1, 2]
sw:11,sv:15,[0, 1]
sw:9,sv:13,[0, 2]
sw:8,sv:10,[0]
sw:9,sv:15,[1, 2, 3]
sw:8,sv:12,[1, 3]
sw:6,sv:10,[2, 3]
----------------

问题

为什么需要遍历所有物品(3,1,5,7,8)计算最大价值? ​

dynamicPrograming4KnapsackProblem代码实现每次仅判断了是否包含物品3的场景,不包含是否包含其他物品的场景。 ​

假设仅遍历一边物品3,那么得到的结论是:最大价值包含物品3,或不包含。但是对于物品1没有判断是否包含,物品3包含或不包含两个代码分支可能都包含物品1,因此需要遍历计算所有物品,然后选择大价值价值组合 ​

总结

对于背包问题的变形有很多,只要理解了01背包问题,其他问题解决方案都是类似的。除了本文所聊到的方法外比如还可以参考弗洛伊德算法/迪彻斯特拉算法,或者构建一个图进行广序/深度优先遍历来解决背包问题。文章中的代码存在大量的重复计算,感兴趣的朋友可以尝试优化优化^_^