前言
动态规划专题,从简到难通关动态规划。
01背包优化
在之前的路径规划当中,我们已经使用过滚动数组来优化过dp数组了,主要是我们需要的并不是整个的过程,只需要结果,并且原本的二维dp数组每一层之间是存在关联的,就可以考虑进行压缩
压缩到一维dp数组后,每次只需要存储一维数组dp,而不需要存储二维数组dp[i][j],节省了存储空间。同时,在一维数组中,连续的元素存储在相邻的内存位置,程序更容易缓存,因此可以提高缓存命中率,从而减少内存访问时间,进而提高程序的效率。
之前我们用动态规划解决了简单的 01 背包问题,通过新建了二维数组 dp[i][j] 用于保存从下标为[0-i]的物品⾥任意取,放进容量为 j 的背包,价值总和最⼤是多少。
压缩到一维的话,可以抛弃掉 i 这个维度的报错,只保存 dp[j] 也就是代表了 j 大小的背包,用所以的物品去装,得到的最大值是多少
| 物品/背包重量 | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| 物品0 | 0 | 15 | 15 | 15 | 15 |
| 物品1 | 0 | 15 | 15 | 20 | 35 |
| 物品2 | 0 | 15 | 15 | 20 | 35 |
回顾一下之前二维dp的定义
在二维DP中,状态转移方程是这样的:
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
对于第 i 件物品,考虑两种情况:
-
(1)当不选取第 i 件物品时,即
dp[i][j]=dp[i-1][j]; -
(2)当选择第 i 件物品时,即
dp[i][j]=dp[i-1][j-weight[i]] + value[i]。
在这个方程中,我们发现,只有 dp[i-1][j] 和 dp[i-1][j-w[i]] 这两个状态会被用到,我们可以考虑将它们用一维数组来表示,从而将二维DP压缩为一维DP。
具体地,我们可以将 dp 数组的第二个下标 i 压缩掉,在一维数组 dp 中,用 dp[j] 表示前 i-1 个物品,背包容量为 j 时的最大价值。这时,状态转移方程可以改写如下:
-
(1)当不选取第 i 件物品时,即
dp[j]=dp[j]; -
(2)当选择第 i 件物品时,即
dp[j]=dp[j-weight[i]] + value[i]。
在这个方程中,我们发现,dp[j] 和 dp[j-weight[i]] 两个状态的下标之差为 weight[i],因此在进行状态转移时,需要从后往前进行更新,保证每个状态在更新时,其依赖的较早的状态不会被覆盖。
根据上面的例子来看,我们初始化的第一个数组dp[j] 为 [ 0,15,15,15,15 ]
然后向下进行滚动,每次滚动,对于的 j 的位置上有两种选择,要嘛选择上面的同一个位置的最大值 dp[j] = dp[j],要嘛选择使用新的物品,那么背包就需要扣掉对应的重量,加上对应的价值,也就是 dp[j] = dp[j-weight[i]] + value[i]
根据这个就可以遍历滚动数组
function knapSack(capacity, weights, values) {
let n = weights.length
let dp = new Array(capacity + 1).fill(0)
for (let i = 0; i < n; i++) {
for (let j = capacity; j >= weights[i]; j--) {
dp[j] = Math.max(dp[j], dp[j - weights[i]] + values[i])
}
}
return dp[capacity]
}
let weights = [1, 3, 4]
let values = [15, 20, 30]
let capacity = 4
let maxV = knapSack(capacity, weights, values)
console.log(maxV) // 输出35