動(dòng)態(tài)規(guī)劃問(wèn)題

動(dòng)態(tài)規(guī)劃(英語(yǔ):Dynamic programming赶袄,簡(jiǎn)稱(chēng)DP)是一種通過(guò)把原問(wèn)題分解為相對(duì)簡(jiǎn)單的子問(wèn)題的方式求解復(fù)雜問(wèn)題的方法沪停。

動(dòng)態(tài)規(guī)劃常常適用于有重疊子問(wèn)題最優(yōu)子結(jié)構(gòu)性質(zhì)的問(wèn)題浆兰,動(dòng)態(tài)規(guī)劃方法所耗時(shí)間往往遠(yuǎn)少于樸素解法雨饺。

動(dòng)態(tài)規(guī)劃的基本思想:若要解一個(gè)給定問(wèn)題荐吉,我們需要解其不同部分(即子問(wèn)題)对省,再合并子問(wèn)題的解以得出原問(wèn)題的解蝗拿。

通常許多子問(wèn)題非常相似,為此動(dòng)態(tài)規(guī)劃法試圖僅僅解決每個(gè)子問(wèn)題一次蒿涎,從而減少計(jì)算量:一旦某個(gè)給定子問(wèn)題的解已經(jīng)算出哀托,則將其記憶化存儲(chǔ),以便下次需要同一個(gè)子問(wèn)題解之時(shí)直接查表劳秋。這種做法在重復(fù)子問(wèn)題的數(shù)目關(guān)于輸入的規(guī)模呈指數(shù)增長(zhǎng)時(shí)特別有用仓手。


背包問(wèn)題(Knapsack problem)
一種組合優(yōu)化的NP完全問(wèn)題。問(wèn)題可以描述為:給定一組物品玻淑,每種物品都有自己的重量和價(jià)格嗽冒,在限定的總重量?jī)?nèi),我們?nèi)绾芜x擇补履,才能使得物品的總價(jià)格最高辛慰。問(wèn)題的名稱(chēng)來(lái)源于如何選擇最合適的物品放置于給定背包中。

關(guān)于各種背包問(wèn)題的講解詳見(jiàn):背包問(wèn)題九講

這里給出01背包問(wèn)題完全背包問(wèn)題的JavaScript實(shí)現(xiàn)
另外干像,“換零錢(qián)問(wèn)題”也是背包問(wèn)題中的一種帅腌。

01背包問(wèn)題
有N件物品和一個(gè)容量為V的背包驰弄。放入第i件物品耗費(fèi)的空間是Ci,得到的價(jià)值是Wi速客。求解將哪些物品裝入背包可使價(jià)值總和最大戚篙。

這是最基礎(chǔ)的背包問(wèn)題。
特點(diǎn):每種物品僅有一件溺职,可以選擇放或不放岔擂。

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



【“將前i件物品放入容量為v的背包中”這個(gè)子問(wèn)題浪耘,若只考慮第i件物品的策略(放或不放)乱灵,那么就可以轉(zhuǎn)化為一個(gè)只和前i ? 1件物品相關(guān)的問(wèn)題。如果不放第i件物品七冲,那么問(wèn)題就轉(zhuǎn)化為“前i ? 1件物品放入容量為v的背包中”痛倚,價(jià)值為F [i ? 1, v];如果放第i件物品澜躺,那么問(wèn)題就轉(zhuǎn)化為“前i ? 1件物品放入剩下的容量為v ? Ci的背包中”蝉稳,此時(shí)能獲得的最大價(jià)值就是F [i ? 1, v ? Ci]再加上通過(guò)放入第i件物品獲得的價(jià)值Wi【虮桑】


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

完全背包問(wèn)題
有N種物品和一個(gè)容量為V 的背包耘戚,每種物品都有無(wú)限件可用。放入第i種物品的耗費(fèi)的空間是Ci操漠,得到的價(jià)值是Wi收津。求解:將哪些物品裝入背包,可使這些物品的耗費(fèi)的空間總和不超過(guò)背包容量浊伙,且價(jià)值總和最大撞秋。

這個(gè)問(wèn)題非常類(lèi)似于01背包問(wèn)題,所不同的是每種物品有無(wú)限件吧黄。也就是從每種物品的角度考慮部服,與它相關(guān)的策略已并非取或不取兩種,而是有取0件拗慨、取1件廓八、取2件……直至取?V /Ci?件等很多種。

如果仍然按照解01背包時(shí)的思路赵抢,令F [i, v]表示前i種物品恰放入一個(gè)容量為v的背包的最大權(quán)值剧蹂。仍然可以按照每種物品不同的策略寫(xiě)出狀態(tài)轉(zhuǎn)移方程,像這樣:


這跟01背包問(wèn)題一樣有O(V N)個(gè)狀態(tài)需要求解烦却,但求解每個(gè)狀態(tài)的時(shí)間已經(jīng)不是常數(shù)了宠叼,求解狀態(tài)F [i, v]的時(shí)間是O(v / Ci),總的復(fù)雜度可以認(rèn)為是O(NV Σ(v / Ci)),是比較大的冒冬。
將01背包問(wèn)題的基本思路加以改進(jìn)伸蚯,得到了這樣一個(gè)清晰的方法。這說(shuō)明01背包問(wèn)題的方程的確是很重要简烤,可以推及其它類(lèi)型的背包問(wèn)題剂邮。但我們還是要試圖改進(jìn)這個(gè)復(fù)雜度。





最少換零錢(qián)問(wèn)題
如果我們有面值為1元横侦、3元和5元的硬幣若干枚挥萌,如何用最少的硬幣湊夠22元?求出最少硬幣數(shù)枉侧。

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))
}
// 測(cè)試
MinReplaceChange(22, [1, 3, 5])
// 輸出
Least: 6

最長(zhǎng)遞增子序列longest increasing subsequence)問(wèn)題
在一個(gè)給定的數(shù)值序列中引瀑,找到一個(gè)子序列,使得這個(gè)子序列元素的數(shù)值依次遞增榨馁,并且這個(gè)子序列的長(zhǎng)度盡可能地大憨栽。最長(zhǎng)遞增子序列中的元素在原序列中不一定是連續(xù)的。

給定一個(gè)長(zhǎng)度為n的數(shù)組a[0], a[1], a[2]..., a[n-1]辆影,找出一個(gè)最長(zhǎng)的單調(diào)遞增子序列(注:遞增的意思是對(duì)于任意的i < j徒像,都滿(mǎn)足a[i] < a[j]黍特,此外子序列的意思是不要求連續(xù)蛙讥,順序不亂即可)。
例如:給定一個(gè)長(zhǎng)度為6的數(shù)組: [5灭衷, 6次慢, 7, 1翔曲, 2迫像, 8],則其最長(zhǎng)的單調(diào)遞增子序列為[5瞳遍,6闻妓,7,8]掠械,長(zhǎng)度為4由缆。

用dp[i]表示以i結(jié)尾的子序列中LIS的長(zhǎng)度。然后用dp[j] (0 <= j < i)來(lái)表示在i之前的LIS的長(zhǎng)度猾蒂。然后我們可以看到均唉,只有當(dāng)a[i] > a[j]的時(shí)候,我們需要進(jìn)行判斷肚菠,是否將a[i]加入到dp[j]當(dāng)中舔箭。

為了保證我們每次加入都是得到一個(gè)最優(yōu)的LIS,有兩點(diǎn)需要注意:(1)每一次蚊逢,a[i]都應(yīng)當(dāng)加入最大的那個(gè)dp[j]层扶,保證局部性質(zhì)最優(yōu)箫章,也就是我們需要找到max(dp[j] (0 <= j < i));(2)每一次加入之后镜会,我們都應(yīng)當(dāng)更新dp[j]的值炉抒,顯然,dp[i] = dp[j] + 1稚叹。
如果寫(xiě)成遞推公式焰薄,我們可以得到dp[i] = max(dp[j] (0 <= j < i)) + (a[i] > a[j] ? 1 : 0)

JavaScript實(shí)現(xiàn)
【時(shí)間復(fù)雜度: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)
}
// 測(cè)試
LIS([3, 5, 8, 2, 9, 10, 4])
// 輸出
length: 5
list: 3,5,8,9,10

斐波那契數(shù)列
詳見(jiàn)上一篇《斐波那契數(shù)列及其優(yōu)化》扒袖。


漢諾塔問(wèn)題
詳見(jiàn)《漢諾塔問(wèn)題》


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末塞茅,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子季率,更是在濱河造成了極大的恐慌野瘦,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件飒泻,死亡現(xiàn)場(chǎng)離奇詭異鞭光,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)泞遗,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)惰许,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人史辙,你說(shuō)我怎么就攤上這事汹买。” “怎么了聊倔?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵晦毙,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我耙蔑,道長(zhǎng)见妒,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任甸陌,我火速辦了婚禮须揣,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘邀层。我一直安慰自己返敬,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布寥院。 她就那樣靜靜地躺著劲赠,像睡著了一般。 火紅的嫁衣襯著肌膚如雪项秉。 梳的紋絲不亂的頭發(fā)上偷厦,一...
    開(kāi)封第一講書(shū)人閱讀 51,125評(píng)論 1 297
  • 那天,我揣著相機(jī)與錄音颂碧,去河邊找鬼塑煎。 笑死沫换,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的最铁。 我是一名探鬼主播讯赏,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼冷尉!你這毒婦竟也來(lái)了漱挎?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤雀哨,失蹤者是張志新(化名)和其女友劉穎磕谅,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體雾棺,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡膊夹,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了捌浩。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片放刨。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖嘉栓,靈堂內(nèi)的尸體忽然破棺而出宏榕,到底是詐尸還是另有隱情拓诸,我是刑警寧澤侵佃,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站奠支,受9級(jí)特大地震影響馋辈,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜倍谜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一迈螟、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧尔崔,春花似錦答毫、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春耘拇,著一層夾襖步出監(jiān)牢的瞬間撵颊,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工惫叛, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留倡勇,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓嘉涌,卻偏偏與公主長(zhǎng)得像妻熊,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子仑最,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容