搞定动态规划系列(三):背包问题

100 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第7天,点击查看活动详情

背包问题(Knapsack problem)是一种组合优化的NP完全问题。问题可以描述为:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高

背包问题分类:

  • 基础背包问题
  • 完全背包问题
  • 多重背包问题

基础背包(01背包)

有N件物品和一个容量为V的背包。第i件物品的重量是w[i],价值是v[i]。求解将哪些物品装入背包可使这些物品的重量总和不超过背包容量,且价值总和最大。

  • 这个问题的状态dp[i][j]表示将前i件物品装进限重为j的背包可以获得的最大价值, 0<=i<=N, 0<=j<=W
  • 状态转移方程为dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-w[i] + v[i]),要满足条件j > w[i]
  • 可进一步压缩空间,状态转移方程变为dp[j]=Math.max(dp[j], dp[j-w[i]]+v[i]),同样要满足条件j > w[i],此时必须逆向枚举

例题:分割等和子集

题目: 给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

  • 元素的总和是知道的,那么两个子集的和分别为sum/2,如果sum为奇数,因为/2后会出现小数,则一定不能分割,如果sum为偶数,继续处理,target = sum/2

  • 可以找出状态转移方程式(0<=i<=nums.length, 0=<j<=target

    • j>=nums[i]时,有第i个加入和不加入两种情况,任意一个为true,则满足条件dp[i][j] = dp[i-1][j]|dp[i-1][j-nums[i]]
    • j<nums[i]时,第i个肯定不能加入,则dp[i][j]=dp[i-1][j]
  • 代码如下

    function canPartition (nums) {
      let sum = 0, n = nums.length
      if (n < 2) return false
      for (let i = 0; i < n; i++) {
        sum += nums[i]
      }
      // 判断奇偶,奇数返回false
      if (sum & 1) {
          return false
      }
      let target = sum / 2
      let dp = new Array(n).fill(0).map(() => new Array(target + 1))
      for (let i = 0; i < n; i++) {
          // 不选取任何一个数,总和就会为0,所以为true
          dp[i][0] = true
      }
      for (let i = 1; i < n; i++) {
        let num = nums[i]
        for (let j = 1; j <= target; j++) {
            if (j >= num) {
                dp[i][j] = dp[i-1][j] | dp[i-1][j-num]
            } else {
                dp[i][j] = dp[i-1][j]
            }
        }
      }
      return dp[n-1][target]
    };
    

    image.png

  • 跑出的结果有点不尽人意,可以进一步压缩空间,状态由一维数组存储,状态转移方程式则变成dp[j] = dp[j] | dp[j-nums[i]]

    • 这种方案要注意,j需要从大到小遍历,必须逆向枚举因为从小到大,dp[j-nums[i]]的值会是更新后的,不再是上一行的值
    function canPartition (nums) {
      let sum = 0, n = nums.length
      if (n < 2) return false
      for (let i = 0; i < n; i++) {
        sum += nums[i]
      }
      // 判断奇偶,奇数返回false
      if (sum & 1) {
          return false
      }
      let target = sum / 2
      let dp = new Array(target + 1).fill(false)
      dp[0] = true
      for (let i = 1; i < n; i++) {
        let num = nums[i]
        // 这里要注意是从大到小
        for (let j = target; j >= num; j--) {
            dp[j] = dp[j] | dp[j-num]
        }
      }
      return dp[target]
    };
    

    image.png

完全背包

完全背包和01背包的不同点是,每个物品可以出现多次

  • 状态转移方程为dp[i][j] = Math.max(dp[i-1][j], dp[i][j-w[i] + v[i]),要满足条件j > w[i]
  • 压缩空间后,转移方程为dp[j] = Math.max(dp[j], dp[j-w[i] + v[i]),,此时必须正向枚举

例题:零钱兑换

function coinChange (coins, amount) {
  let dp = new Array(amount + 1).fill(amount + 1)
  dp[0] = 0
  for (let i = 0; i < coins.length; i++) {
      for (let j = 1; j <= amount; j++) {
        if (j >= coins[i]) {
            dp[j] = Math.min(dp[j], dp[j-coins[i]]+1)
        }
      }
  }
  return dp[amount] > amount ? -1 : dp[amount]
};

image.png

多重背包

多重背包与前面不同就是每种物品是有限个,每种物品个数不同

参考文档:zhuanlan.zhihu.com/p/93857890