本文已参与「新人创作礼」活动,一起开启掘金创作之路。
背包问题
简而言之:背包承重有限情况如果装入物品的价值最高。细化分类有有三种
- 01背包问题(unbounded knapsack problem):一共有N件物品,第i(i从1开始)件物品的重量为w[i],价值为v[i]。在总重量不超过背包承载上限W的情况下,能够装入背包的最大价值是多少
- 完全背包问题(unbounded knapsack problem):与01背包不同就是每种物品可以有无限多个:一共有N种物品,每种物品有无限多个,第i(i从1开始)种物品的重量为w[i],价值为v[i]。在总重量不超过背包承载上限W的情况下,能够装入背包的最大价值是多少
- 多重背包问题(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(包含当前物品,不包含当前物品),转换为公式为:
- 包含当前物品:maxV(n-1, W - wi) + vi
- 不包含当前物品: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背包问题,其他问题解决方案都是类似的。除了本文所聊到的方法外比如还可以参考弗洛伊德算法/迪彻斯特拉算法,或者构建一个图进行广序/深度优先遍历来解决背包问题。文章中的代码存在大量的重复计算,感兴趣的朋友可以尝试优化优化^_^