01背包总结
题目
有 n 件物品和一个最多能背重量为 w 的背包。第i
件物品的重量是weight[i]
,得到的价值是value[i]
。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
比如有 3 个物品,重量一次为:1,3, 4。价值依次为:15,20,30,背包最大重量为4,如何装物品使得总价值最大?
递推公式
对于背包问题,有一种写法, 是使用二维数组,即dp[i][j]
表示从下标为[0-i]
的物品里任意取,放进容量为j
的背包,价值总和最大是多少。
我们用一个表格来表示dp[i][j]
数组,纵向表示 0 ~ i 个物品,横向表示 0 ~ j 的重量。每一个格子表示物品i
在当前背包容量下的最大价值。物品 0 表示不放物品,重量 0 表示背包的容量为 0。
物品 i \ 背包容量 j | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
物品0 | 0 | 0 | 0 | 0 | 0 |
物品1(1,15) | 0 | 15 | 15 | 15 | 15 |
物品2(3,20) | 0 | 15 | 15 | 20 | 35 |
物品3(4,30) | 0 | 15 | 15 | 20 | 35 |
显然,当不放物品或背包容量为 0 时,背包里物品的最大价值是 0。然后,每件物品,都有放与不放两种情况,我们选择其中教大的一种情况。
对于物品 1:
当背包容量为 1 ,因为 j > i,所以格子是 15,后面也都是 15。
对于物品 2:
- 当容量为 1,不放物品 2 ,则背包里只有上一次放的物品,那么价值则为
dp[i - 1][j]
,也就是15;放物品的话,因为此时背包容量小于物品重量,即j < weight[i - 1]
,所以价值是 0。二选一则格子填 15。 - 当容量为 2,同上,也是 15。
- 当容量为 3, 此时物品 2 能放的下。当不放物品时,总价值等于上个物品的总价值 15,当放物品时,说明前一个物品(i - 1)对应的背包容量为
j - weigh[i - 1]
,即放此物品时,前一个物品的总价值只能是dp[i - 1][j - weight[i - 1]]
。前一个物品的总价值是dp[2 - 1][3 - 3] = 0
,那么此次放入物品的总价值 = 20 + 0,20 > 15 选择20。 - 当容量为 4, 此时物品 2 能放的下。当不放物品时,总价值等于上个物品的总价值 15,当放物品时,说明前一个物品(i - 1)对应的背包容量为
j - weigh[i]
,即放此物品时,上一个物品的总价值只能是dp[i - 1][j - weight[i - 1]]
。前一个物品的总价值是dp[2 - 1][4 - 3] = 15
,那么此次放入物品的总价值 = 20 + 15,35 > 15选择35。 - 依此类推......
特别说明:物品 i 对应的重量是 weight[i - 1] ,因为物品 i 从 0 开始的,多一个虚拟物品。
确定递推公式
那么可以有两个方向推出来dp[i][j]
,
- 不放物品 i:由
dp[i - 1][j]
推出,即背包容量为 j,里面不放物品 i 的最大价值,此时dp[i][j]
就是dp[i - 1][j]
。(其实就是当物品 i 的重量大于背包 j 的重量时,物品 i 无法放进背包中,所以背包内的价值依然和前面相同。) - 放物品 i:由
dp[i - 1][j - weight[i - 1]]
推出,dp[i - 1][j - weight[i - 1]]
为背包容量为j - weight[i - 1]
的时候不放物品i
的最大价值,那么dp[i - 1][j - weight[i - 1]] + value[i]
,就是背包放物品i
得到的最大价值。
二选一,则递推公式为:dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i - 1]] + value[i])
。
代码
public class Knapsack01 {
public static void main(String[] args) {
int w = 4; // 背包容量
int[] weight = {1, 3, 4};
int[] value = {15, 20, 30};
int n = weight.length; // 物品的数量
// 创建dp二维数组, 数组的横向是当前背包重量,范围 0 ~ w, 纵向是物品编号,范围 0 ~ n。
// 纵向0 表示 表示不放物品,横向0表示背包容量为0。因为把物品0和重量0算进去了,所以数组范围+1
int[][] dp = new int[n + 1][w + 1];
// 填充dp数组
for (int i = 1; i <= n; i++) { // 物品编号从1开始,因为0的情况价值是0,不考虑了
for (int j = 1; j <= w; j++) { // 背包容量从1开始,因为0的情况价值是0,不考虑了
// 这里注意,物品编号i从1开始,但是重量数组weight[]的下标从0开始的,第i个物品对应的重量weight[i - 1]
// 包括下面的value[i -1]也是一样的道理,物品i在value数组中的值是value[i-1]
if (j < weight[i - 1]) {
/*
* 当前背包的容量都没有当前物品i大的时候,是不放物品i的
* 那么前i-1个物品能放下的最大价值就是当前情况的最大价值
*/
dp[i][j] = dp[i - 1][j];
} else {
/*
* 当前背包的容量可以放下物品i
* 那么此时分两种情况:
* 1、不放物品i
* 2、放物品i
* 比较这两种情况下,哪种背包中物品的最大价值最大
*/
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i - 1]] + value[i -1]);
}
}
}
// 最大价值是:
System.out.println(dp[n][w]);
// dp 数组信息是:
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= w; j++) {
System.out.print(dp[i][j] + "\t");
}
System.out.println("\n");
}
}
}
怎么找出物品编号
怎么找出产生最大价值的物品编号?回溯法。
// 从最右下角开始遍历,
for (int i = n; i > 0; i--) {
for (int j = w; j > 0; j--) {
// 最右边的格子表示在最大背包容量下的最大价值
// 如果当前格子比上一个格子的值大,说明当前物品放进了背包
// 如果是相等则说明当前物品没有放进背包
if (dp[i][j] > dp[i - 1][j]) {
System.out.println(i);
break;
}
}
}