动态规划-从背包问题入手理解

129 阅读8分钟

动态规划

官方语言:动态规划是一种解决多阶段决策问题的数学优化方法。它将原问题分解成若干个子问题,通过解决子问题并将结果保存下来,避免了重复计算,提高了算法效率。通俗来讲,动态规划算法是解决一类具有重叠子问题和最优子结构性质的问题的有效方法

我认识的动态规划

  1. 什么时候可以用动态规划来解决问题

    我一般是用过判断是否是求最值,比如最大、最小、最多、最少,还有就是最有重叠性的子问题; 比如有些递归算法改写;总结:核心就是得出局部最优最终得出全局最优

  2. 怎么写动态规划

    • 分解问题,找出最小的问题(子问题)的解决方法
    • 找到递推关系,就是如何通过解决子问题来构建解决原问题的那么一种关系
    • 找到合适的存储子问题的解的数据结构,比如数据(一般一维,二维)或矩阵
    • 确定初始条件和边界条件,就是最小子问题的解与递推关系停止的条件
    • 最终就是根据递推关系初始条件开始,一次计算所有子问题的解,最终得到原问题的解

说再多不如实战,来几个实例

用逃不过的“背包问题”来了解练习

01 背包,完全背包,多重背包,分组背包
  1. 01 背包(每件物品只能使用一次)

    有  N 件物品和一个容量是  V 的背包。每件物品只能使用一次。 第  i  件物品的体积是  vi,价值是  wi。 求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
    输出最大价值。

现实场景实际的操作:

完全背包.png 可以推理出几点: 1、放入背包有好几种组合,最终比较这几种组合得出最大值 2、组合比较,物品相同时其实顺序不受影响 3、每一步余下的体积就是一个子问题,都可以进行这样推理 总结就是:每一个子问题都这样推理,假设物品体积都是1,那么每个背包价值很理想化的就是 bp[0]=0,bp[1]=bp[0]+v1,bp[2]=bp[1]+v2,bp[3]=bp[2]+v3,bp[4]=bp[3]+v4...bp[N]=bp[N-1]+vN,当然这里过于理想,物品体积是不确定的i => bp[N] = bp[N-i]+vi

// 背包体积 V,
//物品 list = [ [w1, v1], [w2, v2], ... ] w1 为重量,v1 为价值,
//背包能放入的物品最大价值是多少?
let V = 40
let list = [       [10, 20],
       [20, 10],
       [5, 11],
       [10, 5]
     ]

 //基础版本,根据思路图一步一步来
function knapsack0(V, list) {
   // 需要bp[i-1],所以补下0,为了方便计算
   list.unshift([0, 0])
   // 建立一个二维数组,可以理解为一个表格,每行是一个放置的物品,列是每个体积,单元格里存放对应体积下放置商品的最大价值,来储存数据
   // V的下标来对标容积,所以就需要V+1
   let bp = Array.from({ length: list.length }, () => new Array(V + 1).fill(0))
   // 每个物品只能放一次,循环物品列表
   for (let i = 1; i < list.length; i++) {
      let [w, v] = list[i] // w 为重量,v 为价值
      for (let j = V; j <= V; j--) {
         // 比较前一个物品同体积背包(不放当前物品) 和 背包剩余体积值(前物品剩余体积对应背包的价值) + 当前物品价值
         bp[i][j] = Math.max(bp[i - 1][j], bp[i - 1][j - w] + v)
      }
   }
   return bp[list.length-1][V]
}
// 其实可以发现,二维数组有点多余,只是便于理解,可以优化大者覆盖背包
// 优化成一维
 function knapsack1(V, list) {
   // 背包容量为 0 时,最大价值为 0,这里把下标当作容量的指代,所以多补一个0,让下标和容量对齐
   let bp = new Array(V + 1).fill(0)
   // 每一个物品只能放一次,比较出放哪个物品能得出最大价值,使用循环dp[j]就会不停被覆盖为最大值
   for (let i = 0; i < list.length; i++) {
     let [w, v] = list[i] = list[i]
     // 背包容量大于当前物品重量时,才能放入背包,所以循环以j>=w为开始条件,减少循环次数,倒序保证物品只能被选择一次
     for (let j = V; j >= w; j--) {
        // 比较不放和放情况的值,取大者,不放就是当前(已存在)的背包价值,放的话就是当前物品价值 + 剩余背包空间(对应体积背包的价值)的最大价值
       bp[j] = Math.max(bp[j], bp[j - w] + v)
     }

   }
   return bp[V]
 }

  1. 完全背包(每种物品都有无限件可用)

    有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。 第 i 种物品的体积是 vi,价值是 wi。 求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。

    完全背包比 01 背包问题不同点在于物品无限件可用,在 01 背包的理解基础上,可以理解为你每种物品都是无限的,容积有限,那就是每次放入一种物品,容积从头开始 1 到 V 计算一遍,改倒序为正序就可以

       function knapsack1(V, list) {
          let bp = Array(V + 1).fill(0)
          // 循环n种物品
          for (let i = 0; i < list.length; i++) {
             let [w, v] = list[i]
             // 每次正序循环,使物品可以放入多次
             for (let j = v; j <= V; j++) {
                bp[j] = Math.max(bp[j], bp[j - w] + v)
             }
          }
    
          return bp[V]
       }
    
  2. 多重背包问题(第 i 种物品最多有 si 件)

    有 N 种物品和一个容量是 V 的背包。 第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。 求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。输出最大价值。

    多重背包又是在完全背包的基础上做了限制,物品的无限件,变成了有限件,每种物品还不一样,那影响的就是物品的循环次数

    // list = [ [v1, w1, s1], [v2, w2, s2], ... ] v 为体积,w 为价值,s 为每组物品的数量限制
    function knapsack2(V, list) {
       let bp = Array(V + 1).fill(0)
       for (let i = 0; i < list.length; i++) {
          let [w,v,s] = list[i]
          for (let k = V; k >= w; k--) {
             //这里相当于就是一个内嵌的特殊小背包或者通理解为1个,2个,s个物品作为一个物品来放入
             for (let j = 1; j <= s && w * j <= k; j++) {
                bp[k] = Math.max(bp[k], bp[k - j * w] + j * v)
             }
          }
       }
       return bp[V]
    }
    
  3. 分组背包问题(每组物品包含若干个,但在同一组内,你最多只能选择一件物品)

    给定 N 组物品和一个容量为 V 的背包。 每组物品包含若干个,但在同一组内,你最多只能选择一件物品。 每件物品有其对应的体积和价值。 目标是选择一些物品放入背包,使得背包内物品的总体积不超过背包的容量,同时背包内物品的总价值尽可能大。

    分组背包就是物品分组,且每组只能选择一个,那就是每小组循环时要注意和本小组上一轮循环的比较 Math.max(bp[i-1][j],bp[i][j], bp[i-1][j - w] + v),这样才能保证取到同一容积下最大值

    function knapsack3(V, groups) {
       groups.unshift([])
       let bp = Array.from({ length: groups.length }, () => Array(V + 1).fill(0))
       // 遍历每一组物品
       for (let i = 1; i < groups.length; i++) {
          let group = groups[i]
          // 遍历当前组的每件物品
          for (let k = 0; k < group.length; k++) {
             let [w, v] = group[k]
             // 遍历当前组的每个容量
             for (let j = V; j >= w; j--) {
                // 更新每一组的最大价值,同样的背包容积也要比较上一组的值与当前组每一轮的值,取最大值
                bp[i][j] = Math.max(bp[i-1][j],bp[i][j], bp[i-1][j - w] + v)
             }
          }
       }
    
       // 返回容量为V时,背包的最大价值
       return bp[groups.length - 1][V]
    }
    
    
    //遇到这种数据结构可以优化一波,在理解了之后也可以优化下存储结构,减少占用内存
    function knapsack4(V, groups) {
       // bp[i]表示容量为i时的最大价值
       let bp = new Array(V + 1).fill(0)
    
       // 遍历每一组物品
       for (let group of groups) {
          // 临时数组来保存当前组的结果
          let temp = bp.slice()
          // 遍历当前组物品
          for (let item of group) {
             let [w, v] = item
             // 遍历当前组的每个容量
             for (let j = V; j >= w; j--) {
                // 通过临时数组来更新
                temp[j] = Math.max(temp[j], bp[j - w] + v)
             }
          }
    
          // 更新dp数组
          bp = temp
       }
    
       // 返回容量为V时,背包的最大价值
       return bp[V]
    }
    

    碰到问题,理解问题,不着急动手代码,打打草稿写写画画,理清思路,写起来就没有那么痛苦了,如果直接上手写,靠debug来看问题在哪,费时费力还容易烦躁无法继续学习或者解决问题,实在想不到还有网友们,看看别人的思路再来自己实现,学过来学会了就是自己的!加油共勉!