動態規劃問題

動態規劃(英語:Dynamic programming,簡稱DP)是一種通過把原問題分解為相對簡單的子問題的方式求解復雜問題的方法。

動態規劃常常適用于有重疊子問題最優子結構性質的問題,動態規劃方法所耗時間往往遠少于樸素解法。

動態規劃的基本思想:若要解一個給定問題,我們需要解其不同部分(即子問題),再合并子問題的解以得出原問題的解。

通常許多子問題非常相似,為此動態規劃法試圖僅僅解決每個子問題一次,從而減少計算量:一旦某個給定子問題的解已經算出,則將其記憶化存儲,以便下次需要同一個子問題解之時直接查表。這種做法在重復子問題的數目關于輸入的規模呈指數增長時特別有用。


背包問題(Knapsack problem)
一種組合優化的NP完全問題。問題可以描述為:給定一組物品,每種物品都有自己的重量和價格,在限定的總重量內,我們如何選擇,才能使得物品的總價格最高。問題的名稱來源于如何選擇最合適的物品放置于給定背包中。

關于各種背包問題的講解詳見:背包問題九講

這里給出01背包問題完全背包問題的JavaScript實現
另外,“換零錢問題”也是背包問題中的一種。

01背包問題
有N件物品和一個容量為V的背包。放入第i件物品耗費的空間是Ci,得到的價值是Wi。求解將哪些物品裝入背包可使價值總和最大。

這是最基礎的背包問題。
特點:每種物品僅有一件,可以選擇放或不放。

用子問題定義狀態:即F [i, v]表示前i件物品恰放入一個容量為v的背包可以獲得的最大價值。則其狀態轉移方程便是:



【“將前i件物品放入容量為v的背包中”這個子問題,若只考慮第i件物品的策略(放或不放),那么就可以轉化為一個只和前i ? 1件物品相關的問題。如果不放第i件物品,那么問題就轉化為“前i ? 1件物品放入容量為v的背包中”,價值為F [i ? 1, v];如果放第i件物品,那么問題就轉化為“前i ? 1件物品放入剩下的容量為v ? Ci的背包中”,此時能獲得的最大價值就是F [i ? 1, v ? Ci]再加上通過放入第i件物品獲得的價值Wi?!?/p>

let dp = new Array()
for (let i = 0; i < 1000; i++) {
  dp[i] = new Array()
  for (let j = 0; j < 1000; j++) {
    dp[i][j] = 0
  }
}

function pack(n, capacity, costs, values) {
  if (n < 0 || capacity < 0) return -1
  if (n === 0 || capacity === 0) return 0
  
  for (let i = 0; i <= n; ++i) {
    for (let j = 0; j <= capacity; ++j) {
      if (i > 0 && j >= costs[i - 1]) {
        dp[i][j] = Math.max(dp[i - 1][j], 
                    dp[i - 1][j - costs[i - 1]] + values[i - 1])
      }
    }
  }

  return dp[n][capacity]
}
console.log(pack(4, 10, [1, 3, 4, 5], 
                        [3, 6, 2, 8]))
// 輸出
17

完全背包問題
有N種物品和一個容量為V 的背包,每種物品都有無限件可用。放入第i種物品的耗費的空間是Ci,得到的價值是Wi。求解:將哪些物品裝入背包,可使這些物品的耗費的空間總和不超過背包容量,且價值總和最大。

這個問題非常類似于01背包問題,所不同的是每種物品有無限件。也就是從每種物品的角度考慮,與它相關的策略已并非取或不取兩種,而是有取0件、取1件、取2件……直至取?V /Ci?件等很多種。

如果仍然按照解01背包時的思路,令F [i, v]表示前i種物品恰放入一個容量為v的背包的最大權值。仍然可以按照每種物品不同的策略寫出狀態轉移方程,像這樣:


這跟01背包問題一樣有O(V N)個狀態需要求解,但求解每個狀態的時間已經不是常數了,求解狀態F [i, v]的時間是O(v / Ci),總的復雜度可以認為是O(NV Σ(v / Ci)),是比較大的。
將01背包問題的基本思路加以改進,得到了這樣一個清晰的方法。這說明01背包問題的方程的確是很重要,可以推及其它類型的背包問題。但我們還是要試圖改進這個復雜度。





最少換零錢問題
如果我們有面值為1元、3元和5元的硬幣若干枚,如何用最少的硬幣湊夠22元?求出最少硬幣數。

let dp = [1]

function MinRCIter(aim, faceValueArr) {
  if (aim < 0) { return 100000000 }
  if (aim === 0) { return 0 }

  if (dp[aim]) {
    return dp[aim]
  }

  let localMin = 100000000

  for (let i = 0; i < faceValueArr.length; i++) {
    if (aim === faceValueArr[i]) {
      return 1
    }

    let rest = MinRCIter(aim - faceValueArr[i], faceValueArr)
    localMin = Math.min(localMin, rest + 1)
  }

  dp[aim] = localMin
  return dp[aim]
}

function MinReplaceChange(aim, arr) {
  console.log('Least: ' + MinRCIter(aim, arr))
}
// 測試
MinReplaceChange(22, [1, 3, 5])
// 輸出
Least: 6

最長遞增子序列longest increasing subsequence)問題
在一個給定的數值序列中,找到一個子序列,使得這個子序列元素的數值依次遞增,并且這個子序列的長度盡可能地大。最長遞增子序列中的元素在原序列中不一定是連續的。

給定一個長度為n的數組a[0], a[1], a[2]..., a[n-1],找出一個最長的單調遞增子序列(注:遞增的意思是對于任意的i < j,都滿足a[i] < a[j],此外子序列的意思是不要求連續,順序不亂即可)。
例如:給定一個長度為6的數組: [5, 6, 7, 1, 2, 8],則其最長的單調遞增子序列為[5,6,7,8],長度為4。

用dp[i]表示以i結尾的子序列中LIS的長度。然后用dp[j] (0 <= j < i)來表示在i之前的LIS的長度。然后我們可以看到,只有當a[i] > a[j]的時候,我們需要進行判斷,是否將a[i]加入到dp[j]當中。

為了保證我們每次加入都是得到一個最優的LIS,有兩點需要注意:(1)每一次,a[i]都應當加入最大的那個dp[j],保證局部性質最優,也就是我們需要找到max(dp[j] (0 <= j < i));(2)每一次加入之后,我們都應當更新dp[j]的值,顯然,dp[i] = dp[j] + 1。
如果寫成遞推公式,我們可以得到dp[i] = max(dp[j] (0 <= j < i)) + (a[i] > a[j] ? 1 : 0)。

JavaScript實現
【時間復雜度:O(n ^ 2)】

let dp = [1]
let pre = [null]

function lisIter(endWith, listArr) {
  if (dp[endWith]) { 
    return dp[endWith] 
  }

  let localMaxLen = 1
  for (let i = 0; i < endWith; i++) {
    if (listArr[i] < listArr[endWith]) {
      if (localMaxLen < lisIter(i, listArr) + 1) {
        localMaxLen = lisIter(i, listArr) + 1
        pre[endWith] = i
      }
    }
  }
  
  dp[endWith] = localMaxLen
  return dp[endWith]
}


function LIS(arr) {
  for (let i = 0; i < arr.length; i++) {
    lisIter(i, arr)
  }

  let answer = -1
  let lastNode = -1
  for (let i = 0; i < dp.length; i++) {
    if (answer < dp[i]) {
      answer = dp[i]
      lastNode = i
    }
  }

  const seq = []
  do {
    seq.unshift(arr[lastNode])
    lastNode = pre[lastNode]
  } while(lastNode !== null)

  console.log('length: ' + answer)
  console.log('list: ' + seq)
}
// 測試
LIS([3, 5, 8, 2, 9, 10, 4])
// 輸出
length: 5
list: 3,5,8,9,10

斐波那契數列
詳見上一篇《斐波那契數列及其優化》。


漢諾塔問題
詳見《漢諾塔問題》


最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,327評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,996評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,316評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,406評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,128評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,524評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,576評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,759評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,310評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,065評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,249評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,821評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,479評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,909評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,140評論 1 290
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,984評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,228評論 2 375

推薦閱讀更多精彩內容

  • 回溯算法 回溯法:也稱為試探法,它并不考慮問題規模的大小,而是從問題的最明顯的最小規模開始逐步求解出可能的答案,并...
    fredal閱讀 13,707評論 0 89
  • 動態規劃(Dynamic Programming) 本文包括: 動態規劃定義 狀態轉移方程 動態規劃算法步驟 最長...
    廖少少閱讀 3,319評論 0 18
  • 樹形動態規劃,顧名思義就是樹+DP,先分別回顧一下基本內容吧:動態規劃:問題可以分解成若干相互聯系的階段,在每一個...
    Mr_chong閱讀 1,498評論 0 2
  • 1. (和)最大子序列(連續) 這是一道非常經典的動態規劃的題目,用到的思路我們在別的動態規劃題目中也很常用,以后...
    yangqi916閱讀 2,937評論 0 0
  • NO.1 抓拍影子 拍攝要點 1.確認太陽的位置和影子的方向及長度。 2.盡可能選擇簡單、潔凈的地面,將小飾物放在...
    明先森吖閱讀 8,555評論 1 5