1. 简介
背包问题的大致介绍如下:有一个总容量为 V 的背包,给定一些物品,它们的体积存放在数组 v 中,它们的价值存放在数组 w 中,从这些物品中挑出一部分放入背包中,使得背包的总价值最大,问如何挑选。
2. 0/1背包问题
2.1 解决思路
0/1背包问题的关键点在于每个物品的数量只有一个,则对于某件物品,它要么放入背包,要么不放入背包。我们可以创建一个二维数组f来记录结果,并根据问题列出以下式子:
f[i][j] = Max{f[i-1][j], f[i-1][j-v[i]]+w[i]}
注:f[i][j]表示当挑选范围为前i个物品,且背包总容量只有j时,背包的最大价值。能理解这点很重要。
当背包总容量为 j 时,对于某物品i来说:
- 假如该物品 i 不放入背包,则此时价值为 f[i-1][j]
- 假如该物品 i 放入背包,则此时价值为 f[i-1][j-v[i]]+w[i]
- 然后取这两个值的最大值,保存在 f[i][j] 中
2.2 代码实现
/**
* 0/1背包问题
* @param v 各个物品的体积
* @param w 各个物品的价值
* @param volume 背包的总容量
* @return
*/
public int zeroOneKnapsack(int[] v, int[] w, int volume) {
if (v.length != w.length) {
throw new IllegalArgumentException("体积数组的长度与价值数组的长度不一致!");
} else {
// 为了操作方便,在数组v和数组w的第一个位置插入一个无效值
int[] newV = new int[v.length + 1];
int[] newW = new int[w.length + 1];
for (int i = 0; i < newV.length; i++) {
if (i != 0) {
newV[i] = v[i - 1];
newW[i] = w[i - 1];
} else {
newV[i] = -1;
newW[i] = -1;
}
}
v = newV;
w = newW;
// 真正开始计算
int[][] f = new int[v.length][volume + 1];
for (int i = 1; i < v.length; i++) {
for (int j = 1; j <= volume; j++) {
if (j >= v[i]) {
f[i][j] = Math.max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);
} else {
f[i][j] = f[i - 1][j];
}
}
}
return f[v.length - 1][volume];
}
}
2.3 优化结果集空间
仔细观察式子f[i][j] = Max{f[i-1][j], f[i-1][j-v[i]]+w[i]}并结合代码可以发现,对于外循环i来说,其实每次都是与上一轮循环(第i-1轮)的结果进行比较,而不关心更早的轮次,于是我们可以把结果集优化成一维的。代码如下:
/**
* 0/1背包问题,对记录结果的数组空间进行优化
* @param v 各个物品的体积
* @param w 各个物品的价值
* @param volume 背包的总容量
* @return
*/
public int zeroOneKnapsack2(int[] v, int[] w, int volume) {
if (v.length != w.length) {
throw new IllegalArgumentException("体积数组的长度与价值数组的长度不一致!");
} else {
int[] f = new int[volume + 1];
for (int i = 0; i < v.length; i++) {
// j必须是倒序遍历,保证等式右边的f[j]和f[j - v[i]]都是上一轮计算后的结果
// 假如j是正序,则对于外循环来说,其实是与本轮次进行比较,不符合原来的逻辑“与第i-1轮比较”
for (int j = volume; j >= v[i]; j--) {
if (i == 0) {
f[j] = w[i];
} else {
f[j] = Math.max(f[j], f[j - v[i]] + w[i]);
}
}
}
return f[volume];
}
}
3. 完全背包问题
完全背包问题与0/1背包问题相似,区别仅仅在于在本问题中,每个物品可选的数量是k个,k∈[0, +∞)。正常情况下,再对k进行遍历就能解决了,但是此时复杂度达到了O(n^3)。耗时较高,可以优化成O(n^2),伪代码如下:
for (int i = 0; i < v.length; i++)
for (int j = v[i]; j <= V; j++)
f[j] = Max{f[j], f[j-v[i]]+w[i]};
神奇的地方在于,当对 j 进行正序遍历时,其实就已经对 k 进行了遍历讨论,若不能理解的强烈建议以手写的形式跟进一下整个计算过程,我自己就是这样理解的。最终代码如下:
/**
* 完全背包问题
* @param v 各个物品的体积
* @param w 各个物品的价值
* @param volume 背包的总容量
* @return
*/
public int completeKnapsack(int[] v, int[] w, int volume) {
if (v.length != w.length) {
throw new IllegalArgumentException("体积数组的长度与价值数组的长度不一致!");
} else {
int[] f = new int[volume + 1];
for (int i = 0; i < v.length; i++) {
for (int j = v[i]; j <= volume; j++) {
f[j] = Math.max(f[j], f[j - v[i]] + w[i]);
System.out.print(f[j] + "\t");
}
System.out.println();
}
return f[volume];
}
}