前言
动态规划专题,从简到难通关动态规划。
背包基础
背包问题根据题目的要求不同,大致可以分为4种
- 01背包:每个物品只有一个,不能重复选择,即每个物品要么选中,要么不选。
- 完全背包:每个物品有无限个,可以无限次选择,即每个物品可以选0个或者多个。
- 多重背包:每个物品有有限个,选择次数有限制,即每个物品只能选0个、1个或多个(但是总量不能超过物品提供的数量限制)。
- 分组背包:将物品分成若干组,每组物品有一个共同的特点,最多只能选择一组物品。
我们主要需要了解 01背包 以及 完全背包 的相关问题。
01背包
经典题型:给定一个容量为W的背包和n个物品,每个物品有重量 weight[i] 和价值 value[i],选择物品放到背包中,使得背包中的物品总重量不超过W,且总价值最大。
举一个例子
背包最大重量为 4
物品 0 重量 1 价值 15 物品1 重量 3 价值 20 物品2 重量 4 价值 30
首先我们定义dp数组 dp[i][j], 即dp[i][j] 表⽰从下标为[0-i]的物品⾥任意取,放进容量为 j 的背包,价值总和最⼤是多少。
| 物品/背包重量 | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| 物品0 | |||||
| 物品1 | |||||
| 物品2 |
确定 递推公式
我们得到 dp[i][j] 的方式有两种,
因为是 01 背包,所以当我们碰到一个新的物体,无非就两种选择,选他,或者不选他,也就对应了两种不同的价值
一种是选择不选物体 [i] 那么结果就会变成 dp[i-1][j] ,那么这时候的关系就会变成
dp[i][j] == dp[i-1][j],
另一种是选择物体 [i] 那么我们就需要在体重上为物体 [i] 空出足够的空间,当然了,这个的前提是当前的背包大小比物体 [i] 的重量要大,这种特殊情况在举例递推公式的时候暂时不做考虑。当你选择了 物体[i] 那么背包就还会剩下 [j - weight[i]] 的空间,并且这个些空间是需要物体 0 到 i-1 去分配的,当前就可以直接取dp数组前面的部分,这样我们就能够得到这种选择下的价值公式
dp[i][j] == dp[i - 1][j - weight[i]] + value[i]
而我们dp数组每一项的定义是,在 0-i 的物体中任意选,背包大小为j,选出最大的价值存入 dp[i][j],那就是每一项都要从两个不同的选择中挑出那个大的选择作为数组最后的值。
这样就能够总结出递推公式
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
dp 数组初始化
当背包容量为0的时候,所有物品一定都是不可选的,这个没什么好说的。
| 物品/背包重量 | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| 物品0 | 0 | ||||
| 物品1 | 0 | ||||
| 物品2 | 0 |
再由dp公式来看 i 是由 i-1 得来的,那么i=0这一行也是必须要初始化的,这样才能够进行递推
| 物品/背包重量 | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| 物品0 | 0 | 15 | 15 | 15 | 15 |
| 物品1 | 0 | ||||
| 物品2 | 0 |
确定遍历顺序
双重遍历,遍历物品的同时不断地去遍历背包大小
for (let i = 1; i <= n; i++) {
for (let j = 1; j <= capacity; j++) {
if (j < weights[i - 1]) {
dp[i][j] = dp[i - 1][j]; // 背包容量不足,不选第i个物品
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weights[i - 1]] + values[i - 1]); // 选或不选第i个物品中总价值更大的情况
}
}
}
完整代码
function knapsack(capacity, weights, values) {
const n = weights.length;
const dp = new Array(n + 1).fill(0).map(() => new Array(capacity + 1).fill(0));
for (let i = 1; i <= n; i++) {
for (let j = 1; j <= capacity; j++) {
if (j < weights[i - 1]) {
dp[i][j] = dp[i - 1][j]; // 背包容量不足,不选第i个物品
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weights[i - 1]] + values[i - 1]); // 选或不选第i个物品中总价值更大的情况
}
}
}
return dp[n][capacity];
}
这个函数接受一个背包容量capacity,一个物品重量数组weights,一个物品价值数组values。函数使用二维数组dp[i][j]存储将前i个物品装入容量为j的背包中可达到的最大价值。函数最后返回dp[n][capacity],即前n个物品放入容量为capacity的背包中可达到的最大价值。