背包问题
源自代码随想录:动态规划:关于01背包问题,你该了解这些!
01 背包
有 n
件物品和一个最多能背重量为 w
的背包。第 i
件物品的重量是 weight[i]
,得到的价值是 value[i]
。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
重量 | 价值 | |
---|---|---|
物品0 | 1 | 15 |
物品1 | 3 | 20 |
物品2 | 4 | 30 |
/**
*
* @param {*} weight 物品重量
* @param {*} value 物品的价值
* @param {*} size 背包的种类
* @returns
*/
function knapsack(weight, value, size) {
}
1、回溯法解决 0-1 背包问题
回溯法其实就是一个穷举的过程,将所有可能的情况进行穷举,从中寻找最优解,也就是本题的能装入背包的最大价值。
/**
*
* @param {*} weight 物品重量
* @param {*} value 物品的价值
* @param {*} size 背包的种类
* @returns
*/
function knapsack(weight, value, size) {
// 1. 回溯求解 0 - 1 背包问题
let maxValue = Number.MIN_VALUE;
const backTrack = (i, total, totalValue) => {
// 背包装满了 或者 物品都考察完了
if (total === size || i === weight.length) {
if (totalValue > maxValue) {
maxValue = totalValue;
}
return;
}
// 选择不装物品 i
backTrack(i + 1, total, totalValue);
if (total + weight[i] <= size) {
// 选择装物品 i
backTrack(i + 1, total + weight[i], totalValue + value[i]);
}
};
backTrack(0, 0, 0);
return maxValue;
}
knapsack([1, 3, 4], [15, 20, 30], 4); // 35
时间复杂度:O(2^n),n 为物品的件数
可以看到回溯算法的时间复杂度是指数级别的,相当高,下面使用动态规划求解一下!
2、动态规划解决 0-1 背包问题
使用动态规划五部曲:
- 确定dp数组(dp table)以及下标的含义
dp[i][j]: 表示物品从0 - i 之间任意选取装入容量为 j 的背包的最大价值
理解 dp 数组的含义非常重要,不要一直思考要往背包中装多少物品才能把背包装满,然后求最大值。这里使用动态规划的思想是枚举所有可能的状态,横向是物品,纵向是容量
-
确定递推公式
对于物品 i 只有两种状态,选择装入背包或者不选择装入背包
① 不选择装入背包
如果不选择则和上一个状态保持一致,即
dp[i][j] = dp[i - 1][j];
② 选择装入背包
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
dp[i - 1][j - weight[i]]
为背包容量为j - weight[i]
时不放入物品i
的最大价值,那么dp[i - 1][j - weight[i]] + value[i]
就是背包容量为j - weight[i]
时放入物品i
的最大价值。由于数组索引从
0
开始,而我们定义中的i
是从1
开始计数的,所以value[i-1]
和weight[i-1]
表示第i
个物品的价值和重量。dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i - 1]] + value[i - 1]);
-
dp数组如何初始化
这里在创建 dp 时将数组所有元素先初始化为 0,后续在 迭代过程中物品从 i = 1 开始
-
确定遍历顺序
遍历物品或者遍历背包容量都可以,因为这里的求值是根据 左上角某个元素进行迭代的,遍历顺序不影响结果
-
举例推导dp数组
console.table(dp);
/**
*
* @param {*} weight 物品重量
* @param {*} value 物品的价值
* @param {*} size 背包的种类
* @returns
*/
function knapsack(weight, value, size) {
const length = weight.length;
let dp = new Array(length + 1).fill().map(() => new Array(size + 1).fill(0));
for (let i = 1; i <= length; i++) {
for (let j = 0; j <= size; j++) {
if (weight[i - 1] <= j) {
// 由于数组索引从 0 开始,而我们定义中的 i 是从 1 开始计数的,
// 所以 value[i-1] 和 weight[i-1] 表示第 i 个物品的价值和重量。
dp[i][j] = Math.max(
dp[i - 1][j],
dp[i - 1][j - weight[i - 1]] + value[i - 1]
);
} else {
// 物品的容量大于背包的容量,放不下
dp[i][j] = dp[i - 1][j];
}
}
}
console.table(dp);
return dp[length][size];
}
dp 数组打印如下:
┌─────────┬───┬────┬────┬────┬────┐
│ (index) │ 0 │ 1 │ 2 │ 3 │ 4 │
├─────────┼───┼────┼────┼────┼────┤
│ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │
│ 1 │ 0 │ 15 │ 15 │ 15 │ 15 │
│ 2 │ 0 │ 15 │ 15 │ 20 │ 35 │
│ 3 │ 0 │ 15 │ 15 │ 20 │ 35 │
└─────────┴───┴────┴────┴────┴────┘
3、动态规划二维优化为一维(滚动数组)
使用动态规划五部曲:
-
确定dp数组(dp table)以及下标的含义
dp[j]: 容量为 j 的背包,所背的物品价值可以最大为 dp[j]
-
确定递推公式
dp[j - weight[i]]
表示容量为j - weight[i]
的背包所背的最大价值。dp[j - weight[i]] + value[i]
表示容量为j - 物品 i 重量
的背包 加上物品 i 的价值
。(也就是容量为 j 的背包,放入物品 i 了之后的价值即:dp[j])dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
-
一维dp数组如何初始化
dp[j] 表示:容量为j的背包,所背的物品价值可以最大为dp[j],那么dp[0]就应该是0,因为背包容量为0所背的物品的最大价值就是0。
-
一维 dp 数组遍历顺序
// 遍历物品 for (let i = 0; i < weight.length; i++) { // 再倒序遍历背包容量 for (let j = size; j >= weight[i]; j--) { } }
如果正序遍历
dp[1] = dp[1 - weight[0]] + value[0] = 15 dp[2] = dp[2 - weight[0]] + value[0] = 30
此时dp[2]就已经是30了,意味着物品0,被放入了两次,所以不能正序遍历。
为什么倒序遍历,就可以保证物品只放入一次呢?
倒序就是先算dp[2]
dp[2] = dp[2 - weight[0]] + value[0] = 15 (dp数组已经都初始化为0) dp[1] = dp[1 - weight[0]] + value[0] = 15
所以从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了。
先遍历物品嵌套遍历背包容量,那可不可以先遍历背包容量嵌套遍历物品呢?
不可以!
因为一维dp的写法,背包容量一定是要倒序遍历(原因上面已经讲了),如果遍历背包容量放在上一层,那么每个dp[j]就只会放入一个物品,即:背包里只放入了一个物品。
(这里如果读不懂,就在回想一下dp[j]的定义,或者就把两个for循环顺序颠倒一下试试!)
所以一维dp数组的背包在遍历顺序上和二维其实是有很大差异的!
/**
*
* @param {*} weight 物品重量
* @param {*} value 物品的价值
* @param {*} size 背包的种类
* @returns
*/
function knapsack(weight, value, size) {
let dp = new Array(size + 1).fill(0);
// 遍历物品
for (let i = 0; i < weight.length; i++) {
// 再倒序遍历背包容量
for (let j = size; j >= weight[i]; j--) {
dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
}
}
return dp[size];
}