動態規劃-01揹包問題

揹包問題(Knapsack problem)是一種組合優化的NP徹底問題。問題能夠描述爲: 給定一組物品,每種物品都有本身的重量和價格,在限定的總重量內,咱們如何選擇,才能使得物品的總價格最高。javascript

問題

假設山洞裏共有a,b,c,d,e這5件寶物(不是5種寶物),它們的重量分別是2,2,6,5,4, 它們的價值分別是6,3,5,4,6,如今給你個承重爲 10 的揹包, 怎麼裝揹包,能夠才能帶走最多的財富。java

動態規劃

轉化方程

動態規劃一個關鍵的步驟是獲得狀態轉化方程,物體的價值用 v(k) 表示, 重量用 w(k) 表示,f[i, j] 表示將前 i 個物體放入到容量爲 j 的揹包中的最大價值,則有:數組

求解方法

動態規劃有兩種等價的實現方法:函數

  1. 帶備忘的自頂向下法。此方法按照天然的遞歸形式編寫過程,但過程當中會保存每一個子問題的解(一般保存在一個數組或散列表中)。 當須要一個子問題的解時,過程首先檢查是否已經保存過此解。若是是,則直接返回保存的值,從而節省了時間;不然,按一般方式計算 這個子問題。優化

  2. 自底向上法。這種方法通常須要恰當定義子問題「規模」的概念,使得任何子問題的求解都只依賴於「更小的」子問題的求解。於是 咱們能夠將子問題按規模排序,按由小至大的順序進行求解。當求解某個子問題時,它所依賴的那些更小的子問題都已求解完畢, 結果已經保存。ui

帶備忘的自頂向下方法

下面給出一個帶備忘的自頂向下實現:spa

var v = [6,3,5,4,6]
var w = [2,2,6,5,4]
var c = 10

function bag (v, w, c) {
  function _bag (v, w, c, f, s) {
    // 子問題的規模
    var n = v.length
    // 子問題已經被求解
    if (f[n][c] > 0) {
      return f[n][c]
    }
    // 從剩下的物品中選擇一件
    for (var i = 0; i < n; i++) {
      var newW = w.slice()
      newW.splice(i, 1)
      var newV = v.slice()
      newV.splice(i, 1)
      // 選出來的物品重量大於揹包剩餘容量,則該子問題的解爲0
      if (w[i] > c) {
        return 0
      }
      // 不然遞歸求解,獲得子問題的最大的解及當前選擇的物品
      var maxValue = v[i] + _bag(newV, newW, c - w[i], f, s)
      if (f[n][c] < maxValue) {
        f[n][c] = maxValue
        s[n][c] = {v: v[i], w: w[i]}
      }
    }
    // 返回子問題的最大解
    return f[n][c]
  }

  var n = v.length
  // 記錄最大的價值
  var f = []
  // 記錄每一步所作的選擇
  var s = []
  for (var i = 0; i <= n; i++) {
    f[i] = []
    s[i] = []
    for (var j = 0; j <= c; j++) {
      f[i][j] = 0
      s[i][j] = null
    }
  }
  _bag(v, w, c, f, s)

  // 打印兩個二維數組
  console.log(f)
  console.log(s)

  // 從s中獲得所選擇的物品
  var selected = []
  var i = n
  var j = c
  var sum = 0
  do {
    var thing = s[i][j]
    if (thing) {
      selected.push(thing)
      j -= thing.w
      i--
    }
  } while (thing)

  return {
    maxV: f[n][c],
    selected: selected
  }
}
複製代碼

說明code

程序中 f 最後以下所示,其中第一行能夠忽略,這麼作只是爲了讓數組索引從 1 開始,跟上面的公式保持一致:cdn

0 1 2 3 4 5 6 7 8 9 10
null null null null null null null null null null null
null null null null null null null null null null null
0 0 0 null null null null null null null null
0 0 0 0 0 null 6 null null null null
null null null null 6 6 6 null 9 null null
null null null null null null null null null null 15

其中,f[5][10] 就是最後所求的最大價值,即 15。 從上表還能夠知道求解過程當中遞歸求解了哪些問題,即上表中值不爲 null 的那些。 而若是須要知道最後所選擇的物品,還須要藉助 s :blog

0 1 2 3 4 5 6 7 8 9 10
null null null null null null null null null null null
null null null null null null null null null null null
null null null null null null null null null null null
null null { v: 3, w: 2 } { v: 3, w: 2 } { v: 3, w: 2 } null { v: 6, w: 4 } null null null null
null null null null { v: 6, w: 4 } { v: 6, w: 4 } { v: 6, w: 2 } null { v: 3, w: 2 } null null
null null null null null null null null null null { v: 6, w: 2 }

其中,s[i][j] 表示將前 i 個物體放入到容量爲 j 的揹包中時所選擇的第一個物品

如今,讓咱們來理一下這個過程:

  1. s[5][10] 表示將前 5 個物品放到容量爲 10 的揹包中,選擇了物品 { v: 6, w: 2 }
  2. 接下來處理子問題 s[4][8] ,選擇了物品 { v: 6, w: 4 }
  3. 接下來處理子問題 s[3][4] ,選擇了物品 { v: 3, w: 2 }
  4. 接下來處理子問題 s[2][2] ,沒有選擇任何物品。
  5. 獲得最後所選擇的物品爲 { v: 6, w: 2 }, { v: 6, w: 4 }, { v: 3, w: 2 }

自底向上法

下面是自底向上法的實現:

function bag2 (v, w, c) {
  var f = []
  var s = []
  var n = v.length

  for (var i = 0; i <= n; i++) {
    f[i] = []
    s[i] = []
    for (var j = 0; j <= c; j++) {
      f[i][j] = 0
      s[i][j] = 0
    }
  }

  // 遍歷物品
  for (var i = 1; i <= n; i++) {
    var index = i - 1
    // 遍歷容量
    for (var j = 0; j <= c; j++) {
      // 當前物品放入的狀況
      if (w[index] <= j && v[index] + f[i - 1][j - w[index]] > f[i - 1][j]) {
        f[i][j] = v[index] + f[i - 1][j - w[index]]
        s[i][j] = 1
      }
      // 當前物品不放入的狀況
      else {
        f[i][j] = f[i - 1][j]
      }
    }
  }

  return{
    f: f,
    s: s
  }
}
複製代碼

說明

首先,注意到這個事實:物品放入的順序不會影響咱們最後的結果。這裏按照題目中的順序依次考察 每一個物品在每一個容量的狀況下是否放入。

仍然用 f 來記錄最大值,用 s 來記錄選擇。

不過這裏的 s[i][j] 只需標記當前物品是否放入便可, 因此 s[i][j] 取值爲 0 或 1。

f 以下所示:

v w 0 1 2 3 4 5 6 7 8 9 10
- - 0 0 0 0 0 0 0 0 0 0 0
6 2 0 0 6 6 6 6 6 6 6 6 6
3 2 0 0 6 6 9 9 9 9 9 9 9
5 6 0 0 6 6 9 9 9 9 11 11 14
4 5 0 0 6 6 9 9 9 10 11 13 14
6 4 0 0 6 6 9 9 12 12 15 15 15

s 以下所示:

v w 0 1 2 3 4 5 6 7 8 9 10
- - 0 0 0 0 0 0 0 0 0 0 0
6 2 0 0 1 1 1 1 1 1 1 1 1
3 2 0 0 0 0 1 1 1 1 1 1 1
5 6 0 0 0 0 0 0 0 0 1 1 1
4 5 0 0 0 0 0 0 0 1 0 1 0
6 4 0 0 0 0 0 0 1 1 1 1 1

一樣,咱們能夠反向推導出最後的選擇:

  1. s[5][10] 爲 1,該物體放入袋中
  2. 考察 s[4][6],爲 0,說明這個物體不放入
  3. 考察 s[3][6],爲 0, 不放入
  4. 考察 s[2][6],爲 1, 放入
  5. 考察 s[1][4], 爲 1, 放入
  6. 獲得最後所選擇的物品爲 { v: 6, w: 2 }, { v: 3, w: 2 }, { v: 6, w: 4 }

總結

之後碰到動態規劃相關的問題均可以用這個思路來解決了,關鍵在於要構造轉移函數這個模型。 我的感受自頂向下法更加好理解,可是代碼略顯囉嗦了。

相關文章
相關標籤/搜索