01背包问题详解
1. 01背包
1.1 题目
有N件物品和一个容量为V的背包。放入第i件物品占用空间大小Ci,产生的价值是Wi。求解将哪些物品装入背包可使价值总和最大。
1.2 基本思路
这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或者不放。
用子问题来定义状态:即F[i,j]
表示前 i 件物品放入一个容量为 j 的背包可以获得的最大价值。那么其状态转移方程是:
F[i,j]=max{ F[i-1,j], F[i-1,j-Ci]+Wi}
这个方程的意思是,当选择前 i 件物品,放入容量为 j 的背包中的最大价值,只有 2 种情况,对应着第 i 件商品放或者不放入背包。
如果不放第 i 件商品,则等同于问题第 i-1 件商品放入背包为 j 的价值
如果放入第 i 件商品,则等同于问题第 i-1 件商品放入背包容量为 j-ci 的价值加上其本身的价值
所以基于这个状态方程,可以写成如下的代码:
//注:c[i]表示第i件物品占用的空间大小,w[i]表示第i件物品的价值
//首先定义二维价值数组F[i,j],对应着问题第i件物品放入容量为j的背包的价值
var F = [];
for (var i = 0; i < =N; i++) {
F[i] = [];
}
//第0件物品,没有价值,将其赋值为0
for (var j = 0; j <= V; j++) {
F[0][j] = 0;
}
//外循环i表示选择第i件物品
for (var i = 1; i <= N; i++) {
//内循环,表示空间容量大小为j的包
for (var j = c[i]; j <= V; j++) {
F[i][j] = Math.max(F[i - 1][j], F[i - 1][j - c[i] + w[i]]); //状态方程
}
}
1.3 优化空间复杂度
上面的方法时间和空间复杂度都为 O(VN)。时间复杂度不能在优化了。空间复杂度可以优化到 O(V)。
即用一个 V 大小的 F 数组存储,背包容量为 j 时,对应的最大价值。
现在的问题时如何消除 i 的空间,即当选择第 i 件商品时,内循环结束,能够确定F[j]
就是原来的F[i][j]
。
即:
//将
F[i][j] = Math.max(F[i - 1][j], F[i - 1][j - c[i] + w[i]]); //状态方程(1)
//转为
F[j] = Math.max(F[j], F[j - c[i] + w[i]]); //转换方程(2)
内循环的意义是,对于取 i 个物品,背包容量从 0 到 V 能获得的最大价值。
由于状态方程(1)中,第 i 次循环,只用到 i-1 时的 F 结果,那么,去掉 i 是合理的,因为在第 i 次循环时,F 数组已经是 i-1 的时候的结果。
现在需要关注的是:F[j] = Math.max(F[j], F[j - c[i] + w[i]])
这个数组,在 j 从 c[i]到 V 的过程中,我们要确保等式右边 F 数组,一定得是上一轮计算的值。如果循环变量 j 从c[i]
到 V 的顺序递增,那么F[j-c[i]]
用到的肯定是这轮计算后的 F 的值,这样就不对。所以需要将循环顺序从 V 到 c[i]的顺序递减,此时就能确保每次使用的都是上一轮的 F 数组。
即:
//外循环i表示选择第i件物品
for (var i = 1; i < =N; i++) {
//内循环,表示空间容量大小为j的包
for (var j = V; j >= c[i]; j--) {
F[j] = Math.max(F[j], F[j - c[i] + w[i]]); //状态方程
}
}
1.4 初始化细节问题
在求解背包问题时,事实上有两种不太相同的问法。有的题目要求“恰好装满背包”时的最优解,有的题目则并没有要求必须把背包装满。
第一种问法,要求恰好装满背包,那么在初始化时除了f[0]
为 0,其他f[1...V]
均为-Number.NEGATIVE_INFINITY
第二种问法,没有要求必须把背包装满,只是希望价格尽量大,初始化时应该将 F[0...V]
全部设为 0.
原因是,恰好装满背包,只有背包容量为 0 的时候,才能确定是为 0,其他大小都不能确定价值。不要求装满背包,只是希望价格尽量大,那么初始值都可以是 0,表示不装,也是一种解。
1.5 一个常数优化
对于内层循环,可以将循环下限进行改进。从意义上理解,内循环 j 表示空间从c[i]
到容量 V 的的 j 容量大小,即最少是背包大于 c[i]大小的物品才有意义,但是联系外循环放入第 i 个物品,即,i~N 之间的物品肯定不会放入。那么将这些物品的空间进行求和,然后用背包容量减去不放入的,则能得到一个界限 bound=V-sum{w[i]...w[n]}
,此时只要比较 bound 和 c[i]谁大,谁就是循环的下限。
//外循环i表示选择第i件物品
for (var i = 1; i <= N; i++) {
//内循环,表示空间容量大小为j的包
var bound=Math.max(V-sum(c[i]...c[n]),c[i]);
for (var j = V; j >= bound; j--) {
F[j] = Math.max(F[j], F[j - c[i] + w[i]]); //状态方程
}
}
1.6 参考
- 背包问题九讲 2.0 beta1.2 崔添翼
- 背包九讲——全篇详细理解与代码实现