01背包问题:
给定n个物品和一个最多能背重量为w 的背包。数组weights[i]含义为第i个物品的重量,values[i] 含义为第i个物品的价值。每件物品只能用一次,求哪些物品装入背包里可以使得物品价值总和最大?
使用动态规划来解决该问题时首先要搞明白动态规划数组dps的每个值的含义以及递推方程。
解法一:使用二维数组
1、dps数组的定义
dps数组使用二维数组,第一维含义为依次选择是否放入 i 号物体,第二维含义为背包的最大重量。
那么dps[i][j] 就表示当选择是否放入 i 号物品后,背包最大重量为 j 时的最大价值。
举个例子:
dps[0][j]表示当选择是否放入0号物品后,背包最大重量为 j 时的最大价值。
显然,当 j >= weights[0]时候,我们肯定要放入0号物品,因为这时候可以取得最大价值,最大价值就是的0号物品的价值。当 j < weights[0] 时,我们无法放入0 号物品,这样最大价值就是0。
上述例子中,为什么只考虑0 号物品?因为dps数组的定义就是依次考虑给出的物体,i 为0时只用考虑 0 号物体,为1 时则0 号物品已经考虑完了,正在考虑是否要放入1 号物体。
2、dps数组的递推公式
当考虑是否放入i 号物体时,有两种情况,
(1)不放入i 号物体
当不放入i 号物体时,dps[i][j]显然等于考虑完i - 1号物体后的最大价值,即
dps[i - 1][j];
(2)放入 i 号物体
当放入i 号物体时,此时的背包的价值(注意,此时说的是放入i 号物体后的价值,而不是dps[i][j], dps[i][j] 为最大价值)为放 i 物体之前,且背包重量为减去 i 物体重量的最大价值 + 当前i物体的价值,即
dps[i - 1][j - weight[i]] + values[i]
对于dps [i][j] ,上述两种情况取最大值即可,即
dps[i][j] = Math.max(dps[i - 1][j], dps[i - 1][j - weights[i]] + values[i]);
当然,如果 j < weights[i],即当前背包重量小于i号物体重量,就没得选了,只能选中不放入 i 号物体。
3、dps数组的初始化
由上述递推公式,发现dps数组只取决于前一状态时刻,故只需要将第一行初始化即可,
在dps数组定义里举的例子即为dps数组的初始化。
最终代码如下:
public int maxValue(int[] values, int[] weights, int bagWeight) {
// dps[i][j]表示当选择是否放入i号物品后,背包最大重量为j时的最大价值
// dps[0][j]表示当查看是否放入0号物品后,背包最大重量为j时的最大价值
int[][] dps = new int[weights.length][bagWeight + 1];
// 初始化,dps[i][0] 显然为0,因为背包最大重量为0,放不进任何物品
// dps[0][j] 取决于0号物品的重量,当j>weights[0]时,初始化为该物品价值,否则为0
for (int j = 1; j <= bagWeight; j++) {
if (j >= weights[0]) dps[0][j] = values[0];
}
// 遍历物体
for (int i = 1; i < weights.length; i++) {
for (int j = 1; j <= bagWeight; j++) {
// 两种情况,放入i物品和不放i物品
// 放i物品的价值为放i物体之前,且背包重量为减去i物体重量的最大价值 + 当前i物体的价值
// dps[i][j] = dps[i - 1][j - weight[i]] + values[i]
// 不放i物体,最大价值为考虑完是否放入上一个物体的最大价值
if (j < weights[i]) {
dps[i][j] = dps[i - 1][j]; // 当j小于i物体重量时,肯定不能放i物体
} else {
dps[i][j] = Math.max(dps[i - 1][j], dps[i - 1][j - weights[i]] + values[i]);
}
}
}
System.out.println(Arrays.deepToString(dps));
return dps[weights.length - 1][bagWeight];
}
解法二:使用一维数组
1、dps数组的定义
dps数组使用一维数组,dps[i]就表示背包最大重量为 i 时的最大价值。
那怎么进行数组更新?其实本质上同二维数组相同,还是依次考虑是否放入各个物体,然后更新dps数组,直到所有物体都考虑完毕,只不过一维数组变成了自我更新。所以依然需要双重循环。先遍历物体,每考虑一个物体,更新一遍dps数组(即考虑完 i 号物体后的最大价值)。
2、dps数组的递推公式
同样,当考虑是否放入i 号物体时,有两种情况,
(1)不放入i 号物体
当不放入i 号物体时,dps[i]显然等于考虑完i - 1号物体后的最大价值,即其本身不变,此处的 j 依然是背包重量
dps[j] = dps[j];
(2)放入 i 号物体
当放入i 号物体时,此时背包的价值为放 i 物体之前,且背包重量为减去 i 物体重量的最大价值 + 当前i物体的价值,即
dps[j] - weight[i]] + values[i]
对于dps[j],上述两种情况取最大值即可,即
dps[j] = Math.max(dps[j], dps[j - weights[i]] + values[i]);
同样,如果 j < weights[i],即当前背包重量小于i号物体重量,就没得选了,只能选中不放入 i 号物体。
注意:由于dps数组依赖于上一步dps数组(即考虑完 i - 1物体后的dps数组),且是重量大的背包依赖于上一步重量小的背包(映射到dps数组,就是数组右侧依赖于上一步数组左侧,从j - weights[i]可以看出),所以在dps数组自我更新时,要从右往左更新。
3、dps数组的初始化
初始化同二维数组类似
最终代码如下:
public int maxValue1(int[] values, int[] weights, int bagWeight) {
// 一维版本
int[] dps = new int[bagWeight + 1]; // dps[i]表示背包容量为i时最大能有多少价值
// 初始化
for (int i = 0; i <= bagWeight; i++) {
if (i >= weights[0]) dps[i] = values[0];
}
for (int i = 0; i < weights.length; i++) {
for (int j = bagWeight; j > 0; j--) { // 舍弃0号位,重量为0时价值始终为0
// 两种情况,是否选择放入i物品
// 放入, 最大价值就是 dps[j - weights[i]] + values[i]
// 不放入,最大价值为dps[j],相当于i - 1 物品时的重量
if (j >= weights[i]) dps[j] = Math.max(dps[j], dps[j - weights[i]] + values[i]);
}
System.out.println(Arrays.toString(dps));
}
return dps[bagWeight];
}