前言
最近刷了力扣动态规划题目,在此总结一下0-1背包问题和完全背包问题之间的区别,看完此博客
若想再进阶弄明白完全背包问题中的组合数和排列数问题可看此博客:用树形图和剪枝操作带你理解 完全背包问题中组合数和排列数问题
一、0-1背包问题 和 完全背包问题
1.1 0-1背包问题 和 完全背包问题 状态转移方程及其区别
问题 | 状态转移方程 |
---|---|
0-1背包问题 | dp[ i ] [ j ] = Math.max( dp[ i - 1 ] [ j ], dp[ i - 1 ] [ j - weight[ i ] ] + value [ i ]) |
完全背包问题 | dp[ i ] [ j ] = Math.max( dp[ i - 1 ] [ j ], dp[ i ] [ j - weight[ i ] ] + value [ i ]) |
从上表可以发现两个背包问题的状态转移方程的区别在于 dp[ i - 1 ] [ j - weight[ i ] ] + value [ i ]) 和 dp[ i ] [ j - weight[ i ] ] + value [ i ]),这是因为
- 0-1背包时我们计算当前行时的,是通过上一行转移来的,此时可以保证物品i要么没有放进背包,要么只放进背包一次;
- 对于完全背包时因为可以放入多件当前物品,所以当前状态是从上一行和当前行转移来的, dp[ i ] [ j - weight[ i ] ] + value [ i ])表示我们是在前面放入过i物品的前提下放入物品,此时可以保证物品i被放入过多次。
1.2 代码实现
1.2.1 二维数组代码实现
- 0-1背包问题的二维数组实现:
/**
* 0-1背包问题的二维数组实现
*
* @param w w数组表示物品的重量
* @param v v数组表示物品的价值
* @param n n表示指定的背包容量
* @return 可以装入背包里物品的最大价值
*/
public static int bag0_1(int[] w, int[] v, int n) {
int[][] dp = new int[w.length + 1][n + 1];
for (int i = 1; i <= w.length; i++) {
for (int j = 1; j <= n; j++) {
if (j < w[i - 1]) { //当前物品重量超过了当前的背包容量,此时需要舍弃物品i,则此时的最大价值就等于i-1件物品容量等于j时候的价值
dp[i][j] = dp[i - 1][j];
} else { //当前物品重量没有超过当前的背包容量,即当前物品装得下,分为两种情况 1.装 2.不装,取两种情况的最大值
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - w[i - 1]] + v[i - 1]);
}
}
}
return dp[w.length][n];
}
}
- 完全背包问题的二维数组实现:
/**
* 完全背包问题的二维数组实现
* @param w w数组表示物品的重量
* @param v v数组表示物品的价值
* @param n n表示指定的背包容量
* @return 可以装入背包里物品的最大价值
*/
public static int bagComplete(int[] w, int[] v, int n) {
int[][] dp = new int[w.length + 1][n + 1];
for (int i = 1; i <= w.length; i++) {
for (int j = 1; j <= n; j++) {
if (j < w[i - 1]) { //当前物品重量超过了当前的背包容量,此时需要舍弃物品i,则此时的最大价值就等于i-1件物品容量等于j时候的价值
dp[i][j] = dp[i - 1][j];
} else { //当前物品重量没有超过当前的背包容量,即当前物品装得下,分为两种情况 1.装 2.不装,取两种情况的最大值
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - w[i - 1]] + v[i - 1]);
}
}
}
return dp[w.length][n];
}
1.2.3 一维数组代码实现
- 0-1背包问题的一维数组实现:
/**
* 0-1背包问题的一维数组实现
*
* @param w w数组表示物品的重量
* @param v v数组表示物品的价值
* @param n n表示指定的背包容量
* @return 可以装入背包里物品的最大价值
*/
public static int bag0_1(int[] w, int[] v, int n) {
int[] dp = new int[n + 1];
dp[0] = 0;
for (int i = 0; i < w.length; i++) { //遍历物品
for (int j = n; j >= 0; j--) { //遍历背包容量
if (w[i] > j) continue;
dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i]);
}
}
return dp[n];
}
}
- 完全背包问题的一维数组实现:
/**
* 完全背包问题的一维数组实现
* @param w w数组表示物品的重量
* @param v v数组表示物品的价值
* @param n n表示指定的背包容量
* @return 可以装入背包里物品的最大价值
*/
public static int bagComplete(int[] w, int[] v, int n) {
int[] dp = new int[n + 1];
dp[0] = 0;
for (int i = 0; i < w.length; i++) { //遍历物品
for (int j = 0; j <= n; j++) { //遍历背包容量
if (w[i] > j) continue;
dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i]);
}
}
return dp[n];
}
}
小结
对比两个问题的一维数组的实现方式,可以发现在循环遍历背包容量时(内层循环):0-1背包问题是从大到小进行遍历(逆序遍历),而完全背包问题是从小到大进行遍历(正序遍历):
- 如果我们是正序遍历,那么dp[j - w[i]]的值很可能已经被更新成当前层的状态了,也就是已经装了当前物品一次了,此时再 + v[i]就表示又装了一次。
- 如果是逆序遍历,那么dp[j - w[i]]肯定还是上一层的状态,没有被更新,也就是当前物品还没被装,也就是逆序的时候,看到的一直是当前物品没有被装入的状态。
举一个简单的例子:第一个物品体积是1,价值是2,第二个物品体积是2,价值是3,背包容量是5:
(1)枚举第一个物品,正序(从1到5):会发现一直在装物品1,即物品1被多次装进背包,对应完全背包问题中同一个物品可以放入多次
容量 | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
价值 | 0 | 2 | 4 | 6 | 8 | 10 |
(2)枚举第一个物品,逆序(从5到1):会发现物品1只会放进背包一次,对应完全背包问题中同一个物品只可放入一次
容量 | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
价值 | 0 | 2 | 2 | 2 | 2 | 2 |
看完此博客,若想再进阶弄明白完全背包问题中的组合数和排列数问题可看此博客:用树形图和剪枝操作带你理解 完全背包问题中组合数和排列数问题