动态规划-背包问题(滚动数组优化)

169 阅读3分钟

4月日新计划更文活动 第8天

前言

动态规划专题,从简到难通关动态规划。

01背包优化

在之前的路径规划当中,我们已经使用过滚动数组来优化过dp数组了,主要是我们需要的并不是整个的过程,只需要结果,并且原本的二维dp数组每一层之间是存在关联的,就可以考虑进行压缩

压缩到一维dp数组后,每次只需要存储一维数组dp,而不需要存储二维数组dp[i][j],节省了存储空间。同时,在一维数组中,连续的元素存储在相邻的内存位置,程序更容易缓存,因此可以提高缓存命中率,从而减少内存访问时间,进而提高程序的效率。

之前我们用动态规划解决了简单的 01 背包问题,通过新建了二维数组 dp[i][j] 用于保存从下标为[0-i]的物品⾥任意取,放进容量为 j 的背包,价值总和最⼤是多少。

压缩到一维的话,可以抛弃掉 i 这个维度的报错,只保存 dp[j] 也就是代表了 j 大小的背包,用所以的物品去装,得到的最大值是多少

物品/背包重量01234
物品0015151515
物品1015152035
物品2015152035

回顾一下之前二维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