所有的w和v数组下标都按从1开始了,方便描述问题,需要改就在用到w和v的地方下标-1就行。
01背包
给定n个物品,体重分别为w1,w2,w3…,价值分别为v1,v2,v3…,且每个物品只有一件,现在有一个容量为C的背包,求最多能装价值多大的东西。
通过枚举物品种类和背包的容量来解决该问题,现在假设有4件物品,对应的体积和价值如下表所示,背包的容量为10。
i | 1 | 2 | 3 | 4 |
---|---|---|---|---|
w(体积) | 10 | 3 | 4 | 5 |
v(价值) | 3 | 4 | 6 | 7 |
dp状态表如下所示,dp[i][j]表示当前只有i件物品,且背包容量为j时,所能装的最大价值。如果j >= w[i] ,表示当前的容量可以放下物品i,那么此时可以选择放,也可以选择不放:
// 不放物品i 放物品i
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]);
如果j < w[i] ,表示当前的容量放不下物品i,那么此时dp[i][j] = dp[i - 1][j],保持上一层的状态就可以。
初始状态dp[0][j],dp[i][0]均为0,分别表示不装任何物品和背包容量为0时的价值只能是0。这样也是为了避免讨论边界情况,如果当前枚举的背包容量装不下当前物品的时候,那么此时dp[i][j] = dp[i - 1][j],如果没有这个边界,那么数组就会越界访问下标-1。
i/j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 3 |
2 | 0 | 0 | 0 | 4 | 4 | 4 | 4 | 4 | 4 | 4 | 4 |
3 | 0 | 0 | 0 | 4 | 6 | 6 | 6 | 10 | 10 | 10 | 10 |
4 | 0 | 0 | 0 | 4 | 6 | 7 | 7 | 10 | 11 | 13 | 13 |
public int solution(int n, int C, int[] w, int[] v) {
int[][] dp = new int[n + 1][C + 1];
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= C; ++j) {
if (j >= w[i]) {
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]);
} else {
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[n][C];
}
这是最常规的解法,但是空间复杂度还可以再优化,因为通过看dp[i][j]的赋值,每次都只和dp[i - 1][…]有关,也就是只和上一行的状态有关。斐波那契问题中,发现当前状态只和前两步有关,空间上可以优化为只用两个变量来存储。现在发现只和上一行有关,那么只用存一行的状态就可以了,不用(n + 1)行来存储:
public int solution(int n, int C, int[] w, int[] v) {
int[] dp = new int[C + 1];
for (int i = 1; i <= n; ++i) {
for (int j = C; j >= w[i]; -j) {
dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i]);
}
}
return dp[C];
}
这里先不管内层循环为什么是倒序的,先看为什么内层循环不用枚举到1,而是到w[i]就停了,这和上面的讨论其实是一样的:
- j >= w[i]表示当前容量可以装下当前的物品,那还是分为装和不装当前物品,dp[j]就表示不装,还是“上一层”的状态,dp[j - w[i]] + v[i]表示装当前物品。
- 如果装不下,dp[j]还是“上一层”的状态,不会改变,所以就不需要进内层循环了。而之前用二维数组的时候,需要自己手动更新dp[i][j] = dp[i - 1][j],因为数组的初始值都是0,不进内层循环更新的话,没办法保持是“上一层”的状态。
![](https://cdn.jsdelivr.net/gh//Fushier/ImgCloud@main/data/20210610143229.png)
比如当枚举到第3件物品(体积4,价值6),背包容量为3的时候,装不下体积是4的物品,那么需要保持上一层的状态,如果是一维数组,那么你只要不改它的值,就是上一层的状态,二维数组就需要在内层循环手动更新。
下面看最核心的问题,为什么内层循环是倒序的,一定要搞清楚,因为完全背包里内层循环是正序的。先说结论:因为倒序可以保证物品只被拿一次,正序表示物品可以拿多次。
还是用上面的例子,那么dp[]初始状态全是0,现在枚举第一个物品(体积10,价值3),正序倒序结果都一样,dp数组变为:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 3 |
然后枚举第二个物品(体积3,价值4),如果用正序进内层循环for (int j = w[i]; j <= C; ++j)
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 4 | 0 | 0 | 8 | 0 | 0 | 12 | 4 |
这显然是不对的,因为枚举第二件物品的时候,顶破天了价值只能是4,如果用倒序:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 4 | 4 | 4 | 4 | 4 | 4 | 4 | 4 |
其实这里最主要的问题还是在于一维数组,我们在能装下当前物品的情况下进行更新的时候,用的式子是dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i]),用到的都应该是“上一层”的状态:
- 如果我们是正序遍历,那么dp[j - w[i]]的值很可能已经被更新成当前层的状态了,也就是已经装了当前物品一次了,此时再 + v[i]就表示又装了一次。
- 如果是逆序遍历,那么dp[j - w[i]]肯定还是上一层的状态,没有被更新,也就是当前物品还没被装,也就是逆序的时候,看到的一直是当前物品没有被装入的状态。
如果觉得还是不太清楚,认为上面的例子0太多,可以再举一个简单的例子:第一个物品体积是1,价值是2,第二个物品体积是2,价值是3,背包容量是5:
枚举第一个物品,正序:
0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
0 | 2 | 4 | 6 | 8 | 10 |
会发现一直在装,但是逆序的话,看到的都是上一层状态(初始值)0:
0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
0 | 2 | 2 | 2 | 2 | 2 |
然后枚举第二个物品,逆序:
0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
0 | 2 | 3 | 5 | 5 | 5 |
完全背包
相比01背包,变的条件只有每件物品的个数都是无限的,说是无限,其实总重量也不能超过背包容量。还是先考虑二维数组的情况,需要改的地方很少,只需要表示装了物品i之后,还可以接着装物品i即可,这在上面讨论的时候已经有思路了,只要不更新上一层状态,更新当前层状态就可以了:
public int solution(int n, int C, int[] w, int[] v) {
int[][] dp = new int[n + 1][C + 1];
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= C; ++j) {
if (j >= w[i]) {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - w[i]] + v[i]);
} else {
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[n][C];
}
这里j >= w[i],可以装下的时候,表示装入的动作用了dp[i][j - wj[i]] + v[i],而不是i - 1,因为i - 1表示的是上一层的状态,那么i永远是未装入的状态,i表示当前层,可能被更新过,也就是装入过,这时再更新就多装了一次。
那么一维数组的就更好写了,只用把倒序改为正序,可以多次装就行了:
public int solution(int n, int C, int[] w, int[] v) {
int[] dp = new int[C + 1];
for (int i = 1; i <= n; ++i) {
for (int j = w[i]; j <= C; -j) {
dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i]);
}
}
return dp[C];
}