背包问题之多重背包单调优化

126 阅读1分钟

参考文章

本文仅记录个人对背包问题的一些理解,仅做记录用

//  v体积 w价值 s数量 N物品种类 C背包大小
function maxValue(N: number, C: number, s: number[], v: number[], w: number[]) {
  // 处理 0-C ,故有C+1个
  const dp: number[] = Array.from({ length: C + 1 }).fill(0)
  // 遍历物品
  for (let i = 0; i < N; i++) {
    const vi = v[i] // 体积
    const wi = w[i] // 价值
    const si = s[i] // 数量

    // 遍历体积余数
    for (let y = 0; y < vi; y++) {
      // 创建dp的copy,此处便利过程中不需要修改
      const oldDp = [...dp]
      // 单调递减序列,oldDp下标
      const dj = []

      // 遍历余数对体积的等差数列
      for (let num = y; num <= C; num += vi) {
        // 超标,移除第一个
        if (dj[0] !== undefined && dj[0] < num - si * vi) {
          dj.shift()
        }
        // 遍历dj最后一个的价值和当前的价值对比,直到超过当前的价值
        while (
          dj.length &&
          oldDp[dj[dj.length - 1]] - ((dj[dj.length - 1] - y) / vi) * wi <=
            oldDp[num] - ((num - y) / vi) * wi
        ) {
          dj.pop()
        }
        // 将当前值添加到dj中
        dj.push(num)
        // 计算当前最佳
        dp[num] = Math.max(oldDp[num], oldDp[dj[0]] + ((num - dj[0]) / vi) * wi)
      }
    }
  }

  return dp[C]
}
// N = 2, C = 5, v = [1,2], w = [1,2], s = [2,1]
console.log(maxValue(2, 5, [1, 2], [1, 2], [2, 1]))
// 4

通过单调递减队列记录某一个物品新增个数是,是否值得加入 如果更值,则移除单调队列最后不值的,如果不值得,则直接添加到单调队列最后

其中最难理解的是while循环部分,比较是否更值 oldDp记录了上一件物品放入背包的最高价值 当新物品到第1件时,有如下等式,V是放入前占用的体积

newDp[V + vi] = max(oldDp[V + vi], oldDp(V) + wi)

于是有oldDp[V + vi] >= oldDp(V) + wi时,当前物品不放入更优

调整一下oldDp[V + vi] - wi >= oldDp(V) (1

同样的,oldDp[V + 2vi] - 2wi >= oldDp(V)

两边同时加oldDp[V + vi]oldDp[V + 2vi] - 2wi + oldDp[V + vi] >= oldDp(V) + oldDp[V + vi] (2

(2 - (1oldDp[V + 2vi] - 2wi >= oldDp(V + vi) - wi >= oldDp(V)

...递归出更多不等式

当不等式成了时,表示选oldDp[V + nvi]比选oldDp[V + mvi] (n > m)更优,所以移除单调队列最后一个