1、问题描述
有N件物品,一个最多承重为W的背包。
给出一个 weight[] 数组和一个 value[] 数组,weight[i] 表示第 i 件物品的重量,value[i] 表示第 i 件物品的价值。
每件物品只有一个,且只能整体使用,不能分割。求解,将哪些物品装入背包后,总价值最大?
2、问题分析
注意,这个背包问题的要求是,每件物品只有一个,且只能整体使用。它还有两个兄弟姐妹:
-
如果物品可以分割成小份,就属于“贪心背包”。做法是,计算每个物品的“性价比”,然后从大到小装满背包即可。
-
如果物品数量无限,就属于“完全背包”问题。
暴力解法
来分析问题。每件物品只有两种可能性:取或者不取。可以使用回溯算法,搜索所有的情况,记录最大的价值即可。
时间复杂度是O(2n),需要使用动态规划来优化。
3、二维 dp 数组
DP 五部曲:
-
确定 dp 数组及下标的含义。
dp[i][j] 表示,从下标为 [0~i] 的物品中,取任意物品放入容量为 j 的背包的价值总和(这些物品会使得背包的价值最大)
注意 j 不是剩余容量,而是当前假设背包的总容量
-
确定递推公式
可以从两个方向推导 dp[i][j]:
-
由 dp[i-1][j] 推出,即背包容量为 j ,里面不放物品 i 的最大价值,此时 dp[i][j] 就是 dp[i-1][j]
-
由 dp[i-1] [ j-weight[i] ] 推出,dp[i-1] [ j-weight[i] ] 是当背包容量为 j-weight[i] 时,不放物品 i 的最大价值
那么 dp[i-1] [ j-weight[i] ] + value[i] 就是背包放入物品 i 后得到的最大价值
所以递推公式是:dp[i][j] = Math.max(dp[i-1][j], dp[i-1] [ j-weight[i] ] + value[i] ) 其实就是看一看,放入 i 这个物品到底好不好
-
-
初始化 dp 数组
-
如果容量为0,那么价值肯定就是0,所以每个 dp[i][0] = 0
-
递推公式用到了 i-1,所以 dp[0][j] 也必须初始化。它的含义是,各个容量存放0号物品的情况(能放下,价值就是0号的价值。放不下,价值就为0)
-
-
确定遍历顺序
遍历有两个维度:物品、背包重量。
其实从哪个先遍历都可以,这是因为需要的历史数据都来源于 dp[i][j] 的左上角。只要不影响dp的推导,那么怎么遍历都可以。
-
举例推导 dp 数组
如何理解这个状态转移公式
首先明确,dp[i][j] 的含义是:容量为 j 的背包,从 0~i 号物品中,挑选几个放入背包,能得到的最大价值。
接着,如果当前背包的容量 j > weight[i],即这个物品能放入背包中,考虑要不要放入它:
-
如果不放入,则背包变成了“从 0~i 号物品中选择,它的容量是 j,它的价值不变” (之前的状态是,从 0~i-1 号物品中选择,容量为 j )
-
如果选择放入,则背包变成了“从 0~i 号物品中选择,它的容量是 j-weight[i],它的价值增加了value[i]”
我们取这两种状态的最大值,这就是处置物品 i 的最优策略。再来看状态转移公式,就很明了:
dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i]);
接着考虑,为什么这两种状态的值,都已经存在于dp数组中?
-
对于物品的范围,相当于在同一列上做取舍,且依赖的是“靠近上面”的值
-
对于背包的容量,相当于在同一行上做取舍,且依赖的是“靠近左边”的值
我们只需要将第一行(不同容量的背包要装0号物品),和第一列(容量为0的背包要装每个物品)的值进行初始化,后面的值就能依次计算出来。
代码实现
public class Bag {
public static void main(String[] args) {
int[] weight = {1, 3, 4};
int[] value = {15, 20, 30};
int bagWeight = 4;
domain(weight, value, bagWeight);
}
public static void domain(int[] weight, int[] value, int bagWeight){
//dp数组
int[][] dp = new int[value.length][bagWeight + 1];
//初始化
for (int j = weight[0]; j <= bagWeight; j++){
dp[0][j] = value[0];
}
//先遍历物品再遍历背包
for (int i = 1; i < value.length; i++){
for (int j = 0; j <= bagWeight; j++){
//如果剩余容量装不下物品i,那就不装
if (j < weight[i]){
dp[i][j] = dp[i-1][j];
}else{
//能装得下,考虑是否要装它
dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i]);
}
}
}
//dp数组构建完成,给出给定物品个数和背包容量的最大值
System.out.println(dp[value.length-1][bagWeight]);
//打印dp数组
for (int i = 0; i < value.length; i++){
System.out.print(i+"号物品: ");
for (int j = 0; j <= bagWeight; j++){
System.out.print(dp[i][j] + " ");
}
System.out.println();
}
}
}