動(dòng)態(tài)規(guī)劃與背包問題

分治法

當(dāng)我們要處理一個(gè)復(fù)雜的問題時(shí),如果直接求解難度很大,那我們可以采用分治法漩仙,將大問題拆分成可以解決的小問題缆镣。分治法的過程有三步:

  • 分解:將大問題劃分成多個(gè)子問題芽突,子問題的形式與原問題一樣,只是規(guī)模變小了董瞻;
  • 求值:遞歸的將子問題劃分成更小的子問題寞蚌,直到子問題可以直接求解;
  • 合并:將子問題的解組合成原問題的解

分治法或者叫分治思想钠糊,是算法中一個(gè)很重要的思想挟秤。使用到分治法的案例很多,比較常見的如歸并排序抄伍、快速排序艘刚、斐波那契數(shù)列等等。

我們發(fā)現(xiàn)分治法能夠讓我們更容易理解和處理復(fù)雜問題截珍,但是簡單的分治法有一個(gè)缺陷攀甚,可能存在同一個(gè)子問題被重復(fù)計(jì)算的情況,比如背包問題岗喉。此時(shí)我們可以使用動(dòng)態(tài)規(guī)劃來解決秋度。

動(dòng)態(tài)規(guī)劃

動(dòng)態(tài)規(guī)劃與分治法相似,都是通過組合子問題的解來求解原問題钱床。不同的是荚斯,動(dòng)態(tài)規(guī)劃將相同的子問題的解保存起來,保證相同的子問題不會(huì)被重復(fù)計(jì)算诞丽。
動(dòng)態(tài)規(guī)劃解決的問題應(yīng)該具有兩個(gè)要素:

  • 最優(yōu)子結(jié)構(gòu):問題的最優(yōu)解包含子問題的最優(yōu)解
  • 子問題重疊:相同的子問題的值需要保存起來

動(dòng)態(tài)規(guī)劃步驟:

  • 刻畫一個(gè)最優(yōu)解的結(jié)構(gòu)特征
  • 遞歸定義最優(yōu)解的值
  • 計(jì)算最優(yōu)解的值鲸拥,通常采用自底向上的方法(因?yàn)樽缘紫蛏媳苊饬祟l繁的調(diào)用遞歸函數(shù)帶來的開銷)
  • 利用計(jì)算出的信息構(gòu)造一個(gè)最優(yōu)解

前三步是求得一個(gè)最優(yōu)解,而第四步則是在步驟三中維護(hù)一個(gè)額外的信息僧免,以便用來構(gòu)造一個(gè)最優(yōu)解刑赶。

01背包

結(jié)合背包問題來理解動(dòng)態(tài)規(guī)劃:
有n個(gè)物品和一個(gè)承重為m的背包. 給定數(shù)組W表示每個(gè)物品的重量和數(shù)組V表示每個(gè)物品的價(jià)值。問最多能裝入背包的總價(jià)值是多大?
如果每個(gè)物品只有裝和不裝兩種狀態(tài)懂衩,那么稱為01背包問題撞叨。
例如:len = 5, all_weight = 10, item_weights = [2, 2, 6, 5, 4], item_values = [6, 3, 5, 4, 6]

最優(yōu)解的結(jié)構(gòu)特征:
動(dòng)態(tài)規(guī)劃方法的第一步是尋找最優(yōu)子結(jié)構(gòu)金踪,然后就可以利用這種子結(jié)構(gòu)從子問題的最優(yōu)解構(gòu)造出原問題的最優(yōu)解。
假設(shè)(x1牵敷,x2胡岔,…,xn)是01背包問題的最優(yōu)解枷餐,則有(x2靶瘸,x3,…毛肋,xn)是其子問題的最優(yōu)解怨咪,假設(shè)(y2,y3润匙,…诗眨,yn)是上述問題的子問題最優(yōu)解,則有(vy2+vy3+…+vyn)+vx1 > (vx2+vx3+…+vxn)+vx1孕讳。說明(x1匠楚,y2,y3厂财,…芋簿,yn)才是該01背包問題的最優(yōu)解,這與最開始的假設(shè)(x1蟀苛,x2益咬,…,xn)是01背包問題的最優(yōu)解相矛盾帜平,故01背包問題具有最優(yōu)解的結(jié)構(gòu)特征。

遞歸定義最優(yōu)解的值:
前i個(gè)物體放入容量為j的最大價(jià)值為values[i][j]梅鹦,對(duì)于第i個(gè)物品裆甩,我們只有放和不放兩種情況,values[i][j]的值應(yīng)該是兩種情況的較大值齐唆。那么就可以表示為:
values[i][j] = max(values[i - 1][j], values[i - 1][j - item_weights[i - 1]] + item_values[i - 1])
所以values[i][j]問題就拆分成立了max(values[i - 1][j]values[i - 1][j - item_weights[i - 1]] + item_values[i - 1]兩個(gè)子問題嗤栓。

  • values[i][j] = 0 (如果 i == 0 or j == 0)
  • values[i][j] = max(values[i - 1][j], values[i - 1][j - item_weights[i - 1]] + item_values[i - 1])

計(jì)算最優(yōu)解的值&利用計(jì)算出的信息構(gòu)造一個(gè)最優(yōu)解
我們采用自底向上的方案,所以需要兩個(gè)for循環(huán)箍邮,遍歷len和all_weight茉帅,同時(shí)使用雙重?cái)?shù)組values來保存values[i][j]的值,由于Python實(shí)現(xiàn)更加方便锭弊,所以示例代碼我使用了Python2堪澎。
需要注意的是,在計(jì)算哪些物品被選擇時(shí)味滞,就是對(duì)前面選擇物品邏輯的一個(gè)逆向推導(dǎo)樱蛤。

import itertools
def knapsack_01_v1(all_weight, item_weights, item_values, len):
    values = [[0] * (all_weight + 1) for x in xrange(len + 1)]
    for i in xrange(1, len + 1):
        item_weight = item_weights[i - 1]
        item_value = item_values[i - 1]
        for j in xrange(1, all_weight + 1):
            if j >= item_weight:
                values[i][j] = max(values[i - 1][j], values[i - 1][j - item_weight] + item_value)
            else:
                values[i][j] = values[i - 1][j]
    print 'max value: ', max(itertools.chain(*values))
    
    print 'select:'
    w = all_weight
    for i in xrange(len, 0, -1):
        if values[i][w] != values[i - 1][w]:
            print (item_values[i - 1], item_weights[i - 1])
            w -= item_weights[i - 1]

all_weight = 10
item_weights = [2, 2, 6, 5, 4]
item_values = [6, 3,  5, 4, 6]
knapsack_01_v1(all_weight, item_weights, item_values, len(item_weights))

結(jié)果:

max value:  15
select:
(6, 4)
(3, 2)
(6, 2)

時(shí)間復(fù)雜度和空間復(fù)雜度都是O(n*m)钮呀,如果我們只需要求得最優(yōu)解,而不用關(guān)心選擇哪些物品昨凡,我們可以對(duì)空間進(jìn)行優(yōu)化:二維數(shù)組values[len][all_weight]轉(zhuǎn)一維數(shù)組values[all_weight]爽醋。
values[j]表示values[i][j],根據(jù)遞推定義便脊,我們?cè)趺丛撛趺传@得values[i - 1][j]values[i - 1][j - item_weights[i - 1]]呢蚂四?
答案是如果我們對(duì)all_weight進(jìn)行逆遍歷,這樣此時(shí)一維的values[j]代表的就是二維的values[i - 1][j]哪痰,一維的values[j - item_weights[i - 1]]代表的就是二維的values[i - 1][j - item_weights[i - 1]]遂赠。
由于保存的信息被覆蓋,所以沒法求得選了哪些物品妒御。

def knapsack_01_v2(all_weight, item_weights, item_values, len):
    values = [0] * (all_weight + 1)
    for i in xrange(1, len + 1):
        item_weight = item_weights[i - 1]
        item_value = item_values[i - 1]
        for j in xrange(all_weight, item_weight - 1, -1):
            values[j] = max(values[j], values[j - item_weight] + item_value)

    print 'max value: ', max(values)

完全背包

01背包的物品只能選或者不選解愤,如果物品可以選擇無限個(gè)就是完全背包。
理解了01背包乎莉,再來理解完全背包就容易多了送讲,唯一區(qū)別就是物品可以是多個(gè),所以某個(gè)物品的狀態(tài)從裝和不裝轉(zhuǎn)變?yōu)檠b0~k(k = j / item_weight)個(gè)惋啃。

遞歸定義最優(yōu)解的值:

  • values[i][j] = 0 (如果 i == 0 or j == 0)
  • values[i][j] = max(map(lambda x: values[i - 1][j - item_weight * x] + item_value * x, xrange(k+1)))
def knapsack_v1(all_weight, item_weights, item_values, len):
    values = [[0] * (all_weight + 1) for _ in xrange(len + 1)]
    for i in xrange(1, len + 1):
        item_weight = item_weights[i - 1]
        item_value = item_values[i - 1]
        for j in xrange(1, all_weight + 1):
            if j >= item_weight:
                k = j / item_weight
                values[i][j] = max(map(lambda x: values[i - 1][j - item_weight * x] + item_value * x, xrange(k+1)))
            else:
                values[i][j] = values[i - 1][j]
    print 'max value: ', max(itertools.chain(*values))

如果x大于1哼鬓,那么values[i][j - item_weight]必然計(jì)算過j物品個(gè)數(shù)為1的情況,這意味著重復(fù)計(jì)算边灭,所以空間上我們可以做進(jìn)一步的優(yōu)化异希,可以少一層遍歷:

def knapsack_v2(all_weight, item_weights, item_values, len):
    values = [[0] * (all_weight + 1) for _ in xrange(len + 1)]
    for i in xrange(1, len + 1):
        item_weight = item_weights[i - 1]
        item_value = item_values[i - 1]
        for j in xrange(1, all_weight + 1):
            if j >= item_weight:
                values[i][j] = max(values[i - 1][j], values[i][j - item_weight] + item_value)
            else:
                values[i][j] = values[i - 1][j]
    print 'max value: ', max(itertools.chain(*values))

和01背包一樣,我們可以進(jìn)一步把二維數(shù)組優(yōu)化成一維數(shù)組绒瘦,和01背包不一樣的是称簿,完全背包的內(nèi)循環(huán)是升序,原因是計(jì)算某個(gè)物品有x個(gè)的情況需要先計(jì)算這個(gè)物品有x-1個(gè)的結(jié)果惰帽。

def knapsack_v3(all_weight, item_weights, item_values, len):
    values = [0] * (all_weight + 1)
    for i in xrange(1, len + 1):
        item_weight = item_weights[i - 1]
        item_value = item_values[i - 1]
        for j in xrange(item_weight, all_weight + 1):
            values[j] = max(values[j], values[j - item_weight] + item_value)

    print 'max value: ', max(values)

多重背包

多重背包是指物品的選擇有個(gè)數(shù)限制憨降,比如說item_counts = [2, 3, 3, 3, 3]
多重背包可以在knapsack_v1算法中做個(gè)微調(diào)该酗,限制每個(gè)物品數(shù)量授药。

由于每組物品的件數(shù)均不一樣,所以不能使用完全背包的優(yōu)化方法(具體件數(shù)不可控)呜魄,因此采用另一種思路——二進(jìn)制優(yōu)化悔叽。
將每一種物品由1.2.4.8.16.128...的件數(shù)打包,不足一組的零頭重新打包爵嗅,轉(zhuǎn)化為01背包問題娇澎。

混合背包

現(xiàn)在我們來考慮一種更為復(fù)雜的情況,如果可選的物品同時(shí)具有上述三種特性操骡,即:有的物品只能選一個(gè)九火,有的物品可以選擇任意多個(gè)赚窃,有的物品只能選擇有限多個(gè),那么此時(shí)該如何決策呢岔激?

其實(shí)有了上面的總結(jié)和抽象勒极,這種混合背包問題就小菜一碟了。

回顧一下上面的三種背包問題的抽象解虑鼎,就會(huì)發(fā)現(xiàn)他們每次都只會(huì)考慮一種物品辱匿,區(qū)別只在于第i個(gè)物品的可選策略。所以對(duì)于混合背包問題炫彩,同樣也可以一個(gè)一個(gè)物品考慮匾七,如果這個(gè)物品是最多選一個(gè),那么就采用01背包的解決策略江兢,如果是可以選擇任意多個(gè)昨忆,那么就使用完全背包的解決策略,如果只能選擇有限多個(gè)杉允,那么就使用多重背包的解決策略邑贴。

分組背包

01背包的變種,每組物品有若干個(gè)叔磷,同一組內(nèi)的物品最多只能選一個(gè)拢驾,把某個(gè)物品升級(jí)成某組物品。
和01背包幾乎一樣改基,多一層循環(huán)繁疤,遍歷一個(gè)組的物品,判斷選或者不選秕狰。

二維費(fèi)用的背包

包既有重量限制稠腊,又有體積限制,加一個(gè)for循環(huán)判斷體積即可

動(dòng)態(tài)規(guī)劃應(yīng)用

在《算法導(dǎo)論》一書中還介紹了下面情景可以使用動(dòng)態(tài)規(guī)劃來處理

  1. 鋼條切割
  2. 矩陣鏈乘法
  3. 最長公共子序列:序列A=[5, 3, 2, 5, 7, 1, 8, 9]和B=[1, 5, 8, 2, 6, 7, 9]的公共子序列是[5, 2, 7, 9]
  4. 最優(yōu)二叉搜索樹

引用

《算法導(dǎo)論》
背包問題九講 2.0 beta1.2

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末鸣哀,一起剝皮案震驚了整個(gè)濱河市麻养,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌诺舔,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,270評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件备畦,死亡現(xiàn)場(chǎng)離奇詭異低飒,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)懂盐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門褥赊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人莉恼,你說我怎么就攤上這事拌喉∷倌牵” “怎么了?”我有些...
    開封第一講書人閱讀 165,630評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵尿背,是天一觀的道長端仰。 經(jīng)常有香客問我,道長田藐,這世上最難降的妖魔是什么荔烧? 我笑而不...
    開封第一講書人閱讀 58,906評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮汽久,結(jié)果婚禮上鹤竭,老公的妹妹穿的比我還像新娘。我一直安慰自己景醇,他們只是感情好臀稚,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,928評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著三痰,像睡著了一般吧寺。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上酒觅,一...
    開封第一講書人閱讀 51,718評(píng)論 1 305
  • 那天撮执,我揣著相機(jī)與錄音,去河邊找鬼舷丹。 笑死抒钱,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的颜凯。 我是一名探鬼主播谋币,決...
    沈念sama閱讀 40,442評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼症概!你這毒婦竟也來了蕾额?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,345評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤彼城,失蹤者是張志新(化名)和其女友劉穎诅蝶,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體募壕,經(jīng)...
    沈念sama閱讀 45,802評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡调炬,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,984評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了舱馅。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片缰泡。...
    茶點(diǎn)故事閱讀 40,117評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖代嗤,靈堂內(nèi)的尸體忽然破棺而出棘钞,到底是詐尸還是另有隱情缠借,我是刑警寧澤,帶...
    沈念sama閱讀 35,810評(píng)論 5 346
  • 正文 年R本政府宣布宜猜,位于F島的核電站泼返,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏宝恶。R本人自食惡果不足惜符隙,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,462評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望垫毙。 院中可真熱鬧霹疫,春花似錦、人聲如沸综芥。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽膀藐。三九已至屠阻,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間额各,已是汗流浹背国觉。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評(píng)論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留虾啦,地道東北人麻诀。 一個(gè)月前我還...
    沈念sama閱讀 48,377評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像傲醉,于是被迫代替她去往敵國和親蝇闭。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,060評(píng)論 2 355

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