0 / 1 背包问题(0 / 1 knapsack problem)
背包问题(Knapsack problem)是一种组合优化的NP完全问题。问题可以描述为:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。问题的名称来源于如何选择最合适的物品放置于给定背包中。
相似问题经常出现在商业、[组合数学],[计算复杂性理论]、[密码学]和[应用数学]等领域中。
也可以将背包问题描述为决定性问题,即在总重量不超过W的前提下,总价值是否能达到V。
1、题目描述
假设商店中有如下3个商品,他们的重量和价格如下:
索引 | 重量 | 价值 |
---|---|---|
0 | 1 | 1500 |
1 | 4 | 3000 |
2 | 3 | 2000 |
假如你是一个小偷,你有一个重量为4的包,每个商品只能偷一次,请问你怎么偷才会使得最后的价值最大?
2、分析
这种问题一般可以用动态规划很好地解决。但是如果我不用动态规划,而是用搜索所有情况来解决也可以,每个商品都有偷或不偷的选项,所以n个商品就有n^2
种情况,所以用遍历的方法时间复杂度为O(n^2) n为商品的数量
现在我们假设B(k, w)
表示的是前k
个商品,在背包容量为w
的情况下能偷的最高价值
-
当现在面对的第k个物品重量太重时:
B(k, w)
=B(k-1, w)
,代表我在多了一个物品的选择的情况下,仍然和没有这件物品时的选择一样,所以结果也一样(因为我偷不了或者我不偷的情况) -
当第k个物品的重量我可以接受时:
B(k, w)
=B(k-1, w - 这件物品的重量)
+这件物品的价值
代表我如果偷了这件物品,那剩下的w - 这件物品重量
的空间可以容纳的最大价值就是在上一次选择时B(k-1, w - 这件物品的重量)
的值。再加上这件物品的价值就是我偷了这件物品的最大值。
所以,在衡量一个B(k, w)
时,首先看一下能不能偷,能得话看一下偷还是不偷两个的最大值,就是B(k, w)
的值,所以我们回到上面的问题,问题的解就是B(2,4)
的值
我们用**二维数组 dp[][]**来表示整个的过程
可选商品 \ 背包容量 | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
0号商品(1,1500) | 0 | 1500 | 1500 | 1500 | 1500 |
0 ~ 1号商品(4,3000) | 0 | 1500 | 1500 | 1500 | 3000 |
0 ~ 2号商品(3,2000) | 0 | 1500 | 1500 | 2000 | 3500 |
如图中加粗数字 1500
代表的是在有前两个商品,背包容量为2时可以偷的最大价值为1500
图中加粗数字3000
,即在有前2个商品,背包重量为4时,可以偷的最大价值为3000,这个数是这样算的:
- 第二个商品(1号)重量为4,正好满足,如果偷的话所以价值为3000 + 0 = 3000
- 如果不偷的话价值和只有1个商品,背包容量为4的价值一样,1500
- 取最大值为3000
所以问题的关键就在构建这个二维数组
3、实现
/**
* 时间复杂度:O(n * capacity) n为商品数量,capacity为包的大小
* 空间复杂度:O(n * capacity) 可以优化为capacity
*/
public class Main{
/**
* 0/1 背包问题
* @param w w[i]代表i号物品的重量(从0开始)
* @param v v[i]代表i号物品的价值(从0开始)
* @param capacity 代表包的最大容量
* @return 可以偷的商品的最大值
*/
public static int knapsack(int[] w, int[] v, int capacity){
int goods = w.length; // 商品数
int[][] dp = new int[goods][capacity + 1];
// 初始化第一行,因为第一行上层没有元素了,即只有第一个商品时
for(int j = 1; j <= capacity; j++){
if(j >= w[0]) dp[0][j] = v[0];
}
// 前i个商品, 背包容量为j时偷得最大价值
for(int i = 1; i < goods; i++) {
for(int j = 1; j < capacity + 1; j++) {
// 如果容量不够放下第i个商品
if(w[i] > j) {
dp[i][j] = dp[i-1][j];
} else { // 如果可以放下这件商品
dp[i][j] =
Math.max(dp[i-1][j], v[i] + dp[i-1][j-w[i]]);
}
}
}
// System.out.println(Arrays.deepToString(dp));
return dp[goods - 1][capacity];
}
}
用滚动数组优化空间复杂度:
因为如果我们从后往前构建每一行,那上一行保留的就可以在构建时候用
/**
* 时间复杂度:O(n * capacity) n为商品数量,capacity为包的大小
* 空间复杂度:O(capacity)
*/
public class Main{
/**
* 0/1 背包问题
* @param w w[i]代表i号物品的重量(从0开始)
* @param v v[i]代表i号物品的价值(从0开始)
* @param capacity 代表包的最大容量
* @return 可以偷的商品的最大值
*/
public static int knapsack(int[] w, int[] v, int capacity){
int goods = w.length; // 商品数
int[] dp = new int[capacity + 1];
// 前i个商品, 背包容量为j时偷得最大价值
for(int i = 0; i < goods; i++) {
for(int j = capacity; j > 0; j--) {
// 如果能装下就更新,装不下就不更新(上一行的值)
if(j - w[i] >= 0) {
dp[j] = Math.max(dp[j], v[i] + dp[j - w[i]]);
}
}
}
return dp[capacity];
}
}