有 N
件物品和一个最多能背重量为 W
的背包。第i件物品的重量是 weight[i]
,得到的价值是 value[i]
。每件物品只能用一次,求解将哪些物品装入背包里 物品价值总和 最大。
每一件物品其实只有两个状态,取或者不取,所以可以使用回溯法搜索出所有的情况,那么时间复杂度就是O(2^n),这里的n表示物品数量。
所以暴力的解法是指数级别的时间复杂度。进而才需要动态规划的解法来进行优化!
背包最大重量为4。
— | 重量 | 价值 |
---|---|---|
物品0 | 1 | 15 |
物品1 | 3 | 20 |
物品2 | 4 | 30 |
问背包能背的物品最大价值是多少?
(1)dp[i][j]
表示从下标为 i
的物品里任意取,放进容量为 j
的背包,价值总和最大
是多少。
(2)有两个方向可以推出来 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的价值),就是背包放物品i得到的最大价值。dp[i - 1][j - weight[i]]
,是的!)(3)首先从 dp[i][j]
的定义触发,如果背包容量j为0的话,即dp[i][0]
,无论是选取哪些物品,背包价值总和一定为0。
(4)状态转移方程 dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
可以看出 i 是由 i-1 推导出来,那么i为0的时候就一定要初始化。
dp[0][j]
,即:i为0,存放编号0的物品的时候,各个容量的背包所能存放的最大价值。
// 倒叙遍历
for (int j = bagWeight; j >= weight[0]; j--) {
dp[0][j] = dp[0][j - weight[0]] + value[0]; // 初始化i为0时候的情况
}
这个初始化为什么是倒叙的遍历的?正序遍历就不行么?---- 不行!
正序遍历了,那么物品0就会被重复加入多次!例如代码如下:
// 正序遍历
for (int j = weight[0]; j <= bagWeight; j++) {
dp[0][j] = dp[0][j - weight[0]] + value[0];
}
例如 dp[0][1] 是15,到了dp[0][2] = dp[0][2 - 1] + 15; 也就是dp[0][2] = 30 了,那么就是物品0 被重复放入了。
因为 dp[i-1][j-weight[i]] + value[i] 从左上角取值!正序就会重复放入,倒叙就不会,因为前面没值!
(5)初始化代码
// 初始化
int[][] dp = new int[weight.length + 1][bagWeight + 1];
for (int j = bagWeight; j >= weight[0]; j--) {
dp[0][j] = dp[0][j - weight[0]] + value[0];
}
(6)遍历过程
先遍历 物品还是先遍历背包重量呢? 其实都可以!!但是先遍历物品更好理解。
for(int i = 1; i < weight.length; i++) { // 遍历物品:跳开初始化的第一个物品
for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量
if (j < weight[i]) {
dp[i][j] = dp[i - 1][j]; // 背包容量小于当前物品大小,自然放不进去了
} // 这个是为了展现dp数组里元素的变化
else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
}
优化:
for(int i = 1; i < weight.length; i++) { // 遍历物品:跳开初始化的第一个物品
for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量
if (j >= weight[i]) {
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
}
再优化:
// 遍历物品:遍历的是下标,会使用到 weight[i]、value[i]
for(int i = 1; i < weight.length; i++) { // 遍历物品:跳开初始化的第一个物品
for(int j = weight[i]; j <= bagWeight; j++) { // 遍历背包容量
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
(7)全部代码
package org.example;
public class test {
public static void test_2_wei_bag_problem1() {
int[] weight = new int[]{1, 3, 4};
int[] value = new int[]{15, 20, 30};
int bagWeight = 4;
int[][] dp = new int[weight.length + 1][bagWeight + 1];
for (int j = bagWeight; j >= weight[0]; j--) {
dp[0][j] = dp[0][j - weight[0]] + value[0];
}
// 后面讲到滑动数组还可以优化
for (int i = 1; i < weight.length; i++) { // 遍历物品:从第二个物品开始遍历,代表你明白初始化边界条件
for (int j = weight[i]; j <= bagWeight; j++) { // 遍历背包容量
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
// 打印
for (int i = 0; i < weight.length; i++) {
for (int j = 0; j <= bagWeight; j++) {
System.out.println(dp[i][j]);
}
System.out.println();
}
}
public static void main(String[] args) {
test_2_wei_bag_problem1();
}
}
滑动数组:
package org.example;
public class test {
public static void test_2_wei_bag_problem() {
int[] weight = new int[]{1, 3, 4};
int[] value = new int[]{15, 20, 30};
int bagWeight = 4;
int[] dp = new int[bagWeight + 1];
// 把初始化涵盖进去了,但是没有二维数组好理解了,需要深厚的基础才能对滑动数组游刃有余,
// 不然考试的时候又手忙脚乱,基础不牢!
// 滑动数组背包遍历必须倒叙,因为滑动数组覆盖掉了之前值,而二维数组不会!
// 遍历物品:遍历的是下标,会使用到 weight[i]、value[i]
for (int i = 0; i < weight.length; i++) { // 遍历物品
for (int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
}
}
// 放进去的是价值 value[i],dp[j] 就是价值!
System.out.println(dp[bagWeight]);
}
public static void main(String[] args) {
test_2_wei_bag_problem();
}
}
理解成滑动数组:
1)物品 i = 0 的时候,相当于初始化
2)物品 i != 0 的时候,现在是滑动数组,只有一个数组,相当于重复第一次的初始化!
所以要深刻理解初始化!
——
——