c++ 多重背包状态转移方程_动态规划入门——详解经典问题零一背包
本文始發(fā)于個(gè)人公眾號(hào):TechFlow,原創(chuàng)不易,求個(gè)關(guān)注
今天是周三算法與數(shù)據(jù)結(jié)構(gòu)專題的第12篇文章,動(dòng)態(tài)規(guī)劃之零一背包問(wèn)題。
在之前的文章當(dāng)中,我們一起探討了二分、貪心、排序和搜索算法,今天我們來(lái)看另一個(gè)非常經(jīng)典的算法——?jiǎng)討B(tài)規(guī)劃。
在acm-icpc競(jìng)賽領(lǐng)域,動(dòng)態(tài)規(guī)劃是一個(gè)非常大的范疇,當(dāng)中包含了許多變種,而且很多變種難度極大。比如在各種樹(shù)上和圖上以及其他數(shù)據(jù)結(jié)構(gòu)上做動(dòng)態(tài)規(guī)劃,這會(huì)使得問(wèn)題非常復(fù)雜。好在非競(jìng)賽選手并不需要了解到那么深入,一般來(lái)說(shuō),吃透背包九講,就足夠笑傲各種面試了。所以周三的算法專題我們開(kāi)始全新的篇章——背包系列,今天和大家分享背包九講中的第一講,也是最簡(jiǎn)單的零一背包問(wèn)題。
背包和零一背包
沒(méi)有競(jìng)賽經(jīng)驗(yàn)的同學(xué)在看到這個(gè)標(biāo)題的時(shí)候可能會(huì)一頭霧水,動(dòng)態(tài)規(guī)劃和背包有什么關(guān)系。其實(shí)沒(méi)有關(guān)系,我也不是陳奕迅的粉絲,只是當(dāng)初最經(jīng)典的動(dòng)態(tài)規(guī)劃問(wèn)題用背包做了題面,還引發(fā)出了各種變種。后來(lái)在教學(xué)的時(shí)候?yàn)榱朔奖?#xff0c;于是沿用了前人的名稱。
之前我們?cè)诠直I基德偷寶石的問(wèn)題當(dāng)中提到過(guò)背包問(wèn)題,其實(shí)很簡(jiǎn)單,就是說(shuō)我們當(dāng)下有一個(gè)容量是V的背包,和n個(gè)體積分別是v[i],價(jià)值是w[i]的五品。請(qǐng)問(wèn),在背包容量允許的前提下,我們最多能夠獲得多少價(jià)值的物品?
由于每種物品只有一個(gè),也就是物品只有拿和不拿兩種狀態(tài),所以這個(gè)問(wèn)題被稱為零一背包問(wèn)題。
貪心與反例
這種問(wèn)題我們最先想到的就是貪心法,比如優(yōu)先拿價(jià)值大的物品,或者是性價(jià)比高的物品,但是我們很容易構(gòu)思出反例。
舉個(gè)例子,比如背包的容量是10,我們有3個(gè)物品,體積分別是6,5,5,價(jià)值是10,8,8。這個(gè)反例可以證明兩種貪心策略都不生效,因?yàn)閮r(jià)值最大的是10,它的體積是6,我們一旦拿了它就沒(méi)有空間再繼續(xù)獲取其他物品,而顯然拿兩個(gè)5的情況是最優(yōu)的。同樣,體積是6的物品也是性價(jià)比最高的,性價(jià)比優(yōu)先的貪心策略同樣不生效。
實(shí)際上不僅這兩種貪心策略不生效,所有能夠想到的貪心策略都不生效。這個(gè)問(wèn)題看起來(lái)簡(jiǎn)單,但是并不是那么容易解決。實(shí)際上這個(gè)問(wèn)題一直困擾著計(jì)算學(xué)家,直到上世紀(jì)六十年代,動(dòng)態(tài)規(guī)劃算法橫空出世,完美地解決了這個(gè)問(wèn)題。
動(dòng)態(tài)規(guī)劃
動(dòng)態(tài)規(guī)劃算法的英文是dynamic programming,算是很直白的翻譯了。規(guī)劃我們都很好理解,但是動(dòng)態(tài)應(yīng)該怎么理解呢?又怎么來(lái)動(dòng)態(tài)地規(guī)劃呢?關(guān)于這個(gè)問(wèn)題的思考直接關(guān)系到算法的本質(zhì)。
動(dòng)態(tài)規(guī)劃算法的本質(zhì)是狀態(tài)的記錄和轉(zhuǎn)移,我們結(jié)合剛才的問(wèn)題,有沒(méi)有想過(guò)為什么貪心算法不可行?其實(shí)很簡(jiǎn)單,因?yàn)槲覀儧](méi)辦法確定背包什么狀態(tài)是完美的。雖然我們知道背包的容量是V,但是我們并不知道最優(yōu)的情況下我們能裝多少,最優(yōu)的結(jié)束狀態(tài)是什么。我們把空間V看成了一個(gè)狀態(tài)來(lái)進(jìn)行貪心,貪心得到的結(jié)果是最優(yōu)的,但是只是貪心能達(dá)到的狀態(tài)的最優(yōu)解,并不是全局的最優(yōu)解,因?yàn)楸嘲萘康南拗?#xff0c;很有可能我們貪心策略下無(wú)法達(dá)到真正最優(yōu)的狀態(tài)。
用剛才的例子解釋一下上面這段話,在貪心算法下,我們會(huì)選取容量是6,價(jià)值是10的物品,這個(gè)物品拿取了之后背包的狀態(tài)是6,獲取的價(jià)值是10。這個(gè)狀態(tài)是貪心能夠達(dá)到的最終狀態(tài),對(duì)于這個(gè)狀態(tài)而言,它是最優(yōu)解,但是這個(gè)狀態(tài)并不是整體最優(yōu)的情況,因?yàn)樵谪澬牟呗韵?#xff0c;無(wú)法達(dá)到容量10全用完的狀態(tài)。
理解了這個(gè)問(wèn)題之后,再去推導(dǎo)解法就順其自然了。貪心策略可以獲取一些狀態(tài)最優(yōu)的情況,那么我們能不能記錄下所有狀態(tài)能夠達(dá)到的最優(yōu)的情況,最后在這些最優(yōu)的情況當(dāng)中選取一個(gè)最優(yōu)的,它不就是整體最優(yōu)解了嗎?
動(dòng)態(tài)規(guī)劃正是基于上述思路展開(kāi)的,它解決的不是一個(gè)狀態(tài)的最優(yōu)解,而是所有狀態(tài)的最優(yōu)解。
狀態(tài)與轉(zhuǎn)移
看到這里,你肯定還沒(méi)理解動(dòng)態(tài)規(guī)劃算法,但是應(yīng)該已經(jīng)有一些大概的感覺(jué)了。這是對(duì)的,有正確的感覺(jué)是正確認(rèn)識(shí)的前提。我們循序漸進(jìn),再來(lái)看狀態(tài)這個(gè)概念。
我們剛才提了這么多次,究竟?fàn)顟B(tài)是什么呢?這是一個(gè)比較抽象的概念,在不同的問(wèn)題當(dāng)中它有著不同的含義。在背包問(wèn)題當(dāng)中,狀態(tài)指就的是背包容量的使用情況。由于背包問(wèn)題中物品的體積是整數(shù),顯然背包容量的可能性是有限的,這點(diǎn)不起眼,但是很重要,如果狀態(tài)不是整數(shù),那么雖然存在動(dòng)態(tài)規(guī)劃的可能,但是代碼實(shí)現(xiàn)可能比較麻煩。
明白了背包的容量是狀態(tài)之后,我們可以進(jìn)一步想明白,背包的容量是會(huì)變化的。變化的原因是因?yàn)槲覀兺锩娣帕藮|西,放了東西之后,背包的狀態(tài)會(huì)發(fā)生變化,會(huì)從一個(gè)狀態(tài)轉(zhuǎn)移到另一個(gè)狀態(tài)。狀態(tài)的轉(zhuǎn)移中間伴隨著我們放入東西,我們放的物品并不是固定的,而是有多種選擇的,我們決定放入A而不是BC,這是一種決策,決策會(huì)帶來(lái)狀態(tài)的轉(zhuǎn)移,不同的決策會(huì)帶來(lái)不同的轉(zhuǎn)移。
比如當(dāng)前有一個(gè)背包,它的容量是10,我們?cè)谄渲幸呀?jīng)放入了一個(gè)體積是3,價(jià)值是7的物品。如果這個(gè)時(shí)候,我們經(jīng)過(guò)選擇再放入一個(gè)體積是4,價(jià)值是5的物品。那么顯然,背包占用的容量會(huì)變成7,價(jià)值會(huì)變成12。這個(gè)過(guò)程就是一個(gè)經(jīng)典的狀態(tài)轉(zhuǎn)移過(guò)程,這也是整個(gè)動(dòng)態(tài)規(guī)劃算法的核心。
基本的概念和思想已經(jīng)介紹完了,接下來(lái)就是用這些概念來(lái)解決實(shí)際問(wèn)題了。
最優(yōu)狀態(tài)
我們前文說(shuō)了,動(dòng)態(tài)規(guī)劃最后會(huì)獲取所有狀態(tài)的最優(yōu)解,再?gòu)闹羞x取全局最優(yōu)的。那么它是怎么獲取局部最優(yōu)解的呢?
在回答這個(gè)問(wèn)題之前,我們先來(lái)思考兩個(gè)問(wèn)題。
首先,假如我們已經(jīng)知道了背包體積是3時(shí)的最大價(jià)值是5,這時(shí)候我們決定放入一個(gè)體積是4,價(jià)值是5的物品,那么背包的體積會(huì)增加到7,那么這個(gè)時(shí)候獲得的是體積6的最優(yōu)解嗎?
這個(gè)問(wèn)題不難回答,我們稍微想想就知道,很有可能不是。舉個(gè)最簡(jiǎn)單的例子,假如我們有一個(gè)體積是7,價(jià)值是20的物品。那么顯然要比放這兩個(gè)物品更優(yōu)。雖然狀態(tài)3最多能獲得價(jià)值5,狀態(tài)7也可以由狀態(tài)3轉(zhuǎn)移得到,但是這并不一定是最優(yōu)的。也就是說(shuō)最優(yōu)的狀態(tài)轉(zhuǎn)移出去,并不一定也能得到其他狀態(tài)的最優(yōu)值。
我們把問(wèn)題反過(guò)來(lái)就不一樣了,如果我們知道了體積6的最優(yōu)解,并且還知道它是由體積等于4轉(zhuǎn)移得到的,那么我們能不能確定體積4的狀態(tài)也是對(duì)應(yīng)的最優(yōu)解?
這次的答案就變了,是正確的,因?yàn)槿绻w積4時(shí)還有更好的解法,那么體積6理應(yīng)也會(huì)變得更好才對(duì),這和我們的假設(shè)矛盾了。
我們總結(jié)一下上面的兩個(gè)結(jié)論,也就是說(shuō)局部最優(yōu)的情況轉(zhuǎn)移出去并不一定是最優(yōu)的,但是局部最優(yōu)一定也是由其他局部最優(yōu)的狀態(tài)轉(zhuǎn)移得到的。這句話有點(diǎn)像繞口令,但是我覺(jué)得應(yīng)該不難解釋。就好比學(xué)霸去考試,不一定能考第一,但是考到第一的一定是學(xué)霸。局部最優(yōu)就是學(xué)霸,轉(zhuǎn)移就是考試。局部最優(yōu)轉(zhuǎn)移出去并不一定是轉(zhuǎn)移之后狀態(tài)的最優(yōu),有可能還有其他更好的轉(zhuǎn)移策略,但是對(duì)于某個(gè)狀態(tài)最優(yōu)的情況而言,它一定也是從之前的某個(gè)最優(yōu)狀態(tài)轉(zhuǎn)移得到的。
并且狀態(tài)的轉(zhuǎn)移也是有順序的,比如在這題當(dāng)中,背包當(dāng)中放入了物品體積只可能增加,不可能減小,意味著狀態(tài)只能從小的轉(zhuǎn)移到大的。
我們捋一捋思路,已經(jīng)很明確了,狀態(tài)可以轉(zhuǎn)移,狀態(tài)的轉(zhuǎn)移有順序,局部最優(yōu)一定是由其他局部最優(yōu)轉(zhuǎn)移得到的。由于我們并不知道當(dāng)前的轉(zhuǎn)移能否達(dá)到最優(yōu)狀態(tài),所以我們需要用一個(gè)數(shù)組或者是容器來(lái)記錄所有狀態(tài)歷史上曾經(jīng)達(dá)到過(guò)的最值。最后從所有的最值當(dāng)中再選出一個(gè)最值來(lái),就是最后問(wèn)題的解。
到這里,如果是一般的動(dòng)態(tài)規(guī)劃問(wèn)題,已經(jīng)解決了,但是零一背包還有一個(gè)細(xì)節(jié)需要考慮。
無(wú)后效性
我們先來(lái)看下整個(gè)的計(jì)算流程,首先我們需要從最初狀態(tài)開(kāi)始,這個(gè)最初的狀態(tài)很好辦,就是背包是空的時(shí)候,這時(shí)候的價(jià)值是0,體積也是0,這也是它的最優(yōu)狀態(tài),這個(gè)很好理解,因?yàn)槲覀儾荒軣o(wú)中生有。
所以我們從0開(kāi)始轉(zhuǎn)移狀態(tài),狀態(tài)轉(zhuǎn)移伴隨著決策,在這題當(dāng)中體現(xiàn)在選取不同的物品上。我們遍歷物品,作為決策,再遍歷能夠應(yīng)用這些決策的狀態(tài),就拿到了所有的狀態(tài)轉(zhuǎn)移。最后,我們用一個(gè)容器記錄一下所有狀態(tài)轉(zhuǎn)移過(guò)程當(dāng)中達(dá)到的局部最優(yōu)解,于是就結(jié)束了。
這個(gè)過(guò)程看起來(lái)非常正常,沒(méi)有任何異常,但實(shí)際上,問(wèn)題來(lái)了。
我們還用剛才的題面舉例,背包容量是10,3個(gè)物品,體積分別是6,5,5,價(jià)值是10,8,9。我們從0開(kāi)始拿取第三個(gè)物品,轉(zhuǎn)移到了狀態(tài)5,此時(shí)的價(jià)值是9。這個(gè)時(shí)候,我們繼續(xù)往后遍歷的話還會(huì)遍歷到狀態(tài)5,它已經(jīng)是拿取了物品3,價(jià)值9的信息了。因?yàn)橐粋€(gè)物品只能拿一次,所以我們不能再用物品3轉(zhuǎn)移狀態(tài)5,否則就違反了題意。
你可能會(huì)說(shuō)這個(gè)問(wèn)題不難,我們可以在狀態(tài)當(dāng)中也記錄之前做過(guò)的決策嘛,只要在決策的時(shí)候加一個(gè)判斷就好了。
表上面看是因?yàn)槲锲凡荒苤貜?fù)拿的限制,實(shí)際上是因?yàn)槲覀兊臓顟B(tài)之間會(huì)有影響。也就是說(shuō)我們前面做的決策很有可能影響后面的狀態(tài)做決策,這種狀態(tài)之間的前后影響稱為后效性。顯然在有后效性的場(chǎng)景下我們是不能使用動(dòng)態(tài)規(guī)劃算法的,并不是所有問(wèn)題都可以通過(guò)加上判斷解決,我們需要解決后效性這個(gè)本質(zhì)問(wèn)題,也就是說(shuō)我們要想辦法消除后效性。
在這個(gè)問(wèn)題當(dāng)中,這一點(diǎn)很容易做到。我們只需要控制一下?tīng)顟B(tài)和決策的遍歷順序,將之前的決策與之后的決策分開(kāi),使它們互不影響即可。如果我們先遍歷狀態(tài),再遍歷每個(gè)狀態(tài)可以采取的措施,這樣必然會(huì)造成前后影響。因?yàn)榍懊孀隽说臎Q定,后面就不能再做。但是后面并不能感知前面究竟做了什么決定。所以比較好的方法是先遍歷決策,再來(lái)遍歷可以采取這個(gè)決策的狀態(tài)。為了避免決策前后的互相影響,我們采取倒序的方式遍歷狀態(tài)。
我們舉個(gè)例子,假設(shè)背包容量還是10,我們枚舉的第一個(gè)物品體積是3,價(jià)值是5。我們倒敘遍歷狀態(tài)7到0,因?yàn)閷?duì)于大于7的狀態(tài)而言,并不能采取這個(gè)決策(總體積會(huì)超)。因?yàn)閷?duì)于大于7的狀態(tài)而言,我們不能采取這個(gè)決策(總體積會(huì)超過(guò)限制),對(duì)于狀態(tài)7而言,我們可以采取這個(gè)決策,轉(zhuǎn)移到狀態(tài)10。我們并不知道這樣轉(zhuǎn)移會(huì)不會(huì)達(dá)成最優(yōu),所以我們這樣來(lái)記錄:dp[10] = max(dp[10], dp[3] + 5).
我們接著遍歷體積6,可以轉(zhuǎn)移到狀態(tài)9。
由于我們是倒序遍歷,所以當(dāng)我們用狀態(tài)7更新?tīng)顟B(tài)10時(shí),狀態(tài)7本身并沒(méi)有被這個(gè)決策更新過(guò)。即使后面我們?cè)诒闅v到狀態(tài)4時(shí)更新了狀態(tài)7,也不會(huì)影響狀態(tài)10的結(jié)果。因?yàn)槭堑剐虮闅v的,我們不會(huì)再用同一個(gè)策略更新到狀態(tài)10了。如果是正序遍歷,則無(wú)法避免。同樣的物品,我們很有可能會(huì)出現(xiàn),用狀態(tài)1更新?tīng)顟B(tài)4,再用狀態(tài)4更新?tīng)顟B(tài)7,再用狀態(tài)7更新?tīng)顟B(tài)10的情況出現(xiàn)。而這種情況其實(shí)對(duì)應(yīng)了使用了多個(gè)同樣的物品,這就和題意矛盾了。
舉個(gè)例子,假設(shè)有一個(gè)物品體積是2,它的價(jià)值是5。我們遍歷狀態(tài)0的時(shí)候,會(huì)更新?tīng)顟B(tài)2,我們遍歷到了狀態(tài)2,又用同樣的物品更新了狀態(tài)4,得到了10。那么對(duì)于狀態(tài)4而言,它其實(shí)相當(dāng)于拿了2個(gè)這個(gè)物品,也就是說(shuō)被同一個(gè)決策更新了兩次。但是我們的物品最多只有一個(gè),顯然就不對(duì)了。
動(dòng)態(tài)規(guī)劃當(dāng)中因?yàn)闊o(wú)法判斷當(dāng)前枚舉的狀態(tài)的來(lái)源,所以不允許出現(xiàn)后效性,如果解決不了則不能使用動(dòng)態(tài)規(guī)劃。這也是動(dòng)態(tài)規(guī)劃最基本的原則,在這題當(dāng)中,我們是巧妙變換了決策和狀態(tài)枚舉的過(guò)程,消除了后效性。在其他題目當(dāng)中未必相同,我們需要根據(jù)實(shí)際情況進(jìn)行判斷。
如果你在做題思考的過(guò)程當(dāng)中忘記了動(dòng)態(tài)規(guī)劃的前提,就想想零一背包當(dāng)中拿取物品的情況。物品只有一個(gè),只能拿一次。前面拿過(guò)了后面還能拿,就違反了后效性。
狀態(tài)轉(zhuǎn)移方程
我們整理一下剛才關(guān)于狀態(tài)轉(zhuǎn)移的思路,有以下幾點(diǎn):
在這個(gè)問(wèn)題當(dāng)中,決策就是獲取物品,狀態(tài)就是背包容量。由于拿取物品會(huì)引起背包容量變化,并且每個(gè)物品只有一個(gè),為了避免產(chǎn)生后效性,我們需要先枚舉決策,再枚舉狀態(tài),保證每個(gè)決策只在每個(gè)狀態(tài)上最多應(yīng)用一次。在此過(guò)程當(dāng)中,需要一直記錄每個(gè)狀態(tài)的最優(yōu)解。
由于背包的容量是V,我們只需要用一個(gè)容量是V的數(shù)組就足夠記錄所有的狀態(tài)。
dpdp記錄的是所有的狀態(tài),我們用max(dp[v+i.v], dp[v] + i.w)來(lái)更新dp[v+i.v]狀態(tài)的值,由于當(dāng)前的決策不一定比之前的更好,所以要加上max操作,保證每個(gè)狀態(tài)記錄下來(lái)的結(jié)果都是它最優(yōu)的。當(dāng)所有的狀態(tài)的最優(yōu)解都有了之后,顯然整個(gè)問(wèn)題的最優(yōu)解也在其中了。
上面這個(gè)記錄狀態(tài)轉(zhuǎn)移過(guò)程的式子叫做狀態(tài)轉(zhuǎn)移方程,它也是動(dòng)態(tài)規(guī)劃算法的核心概念。很多時(shí)候,在我們解動(dòng)態(tài)規(guī)劃問(wèn)題的時(shí)候,會(huì)在草稿紙上推演狀態(tài)轉(zhuǎn)移方程。如果狀態(tài)轉(zhuǎn)移方程能清楚地列出來(lái),距離寫出代碼也就不遠(yuǎn)了。
代碼
上面的轉(zhuǎn)移方程已經(jīng)非常接近最后的代碼了,真正寫出來(lái)也就只有幾行而已:
dp總結(jié)
關(guān)于零一背包的前后推導(dǎo)以及當(dāng)中所有的概念始末就算是介紹完了,雖然我們用了這么多篇幅來(lái)介紹這個(gè)算法,但是真正寫成代碼也就只有短短幾行。單從代碼行數(shù)來(lái)看,動(dòng)態(tài)規(guī)劃可以說(shuō)是實(shí)現(xiàn)代碼最短的算法了,只是雖然它代碼不長(zhǎng),但是思路并不簡(jiǎn)單,尤其是當(dāng)中的下標(biāo)以及循環(huán)的順序等細(xì)節(jié),希望大家不要掉以輕心。
今天零一背包的問(wèn)題到這里就結(jié)束了,下周的算法專題我們繼續(xù)背包問(wèn)題,來(lái)看看01背包的進(jìn)階版——完全背包和多重背包問(wèn)題,敬請(qǐng)期待。
如果覺(jué)得有所收獲,請(qǐng)順手點(diǎn)個(gè)關(guān)注或者轉(zhuǎn)發(fā)吧,你們的舉手之勞對(duì)我來(lái)說(shuō)很重要。
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎(jiǎng)勵(lì)來(lái)咯,堅(jiān)持創(chuàng)作打卡瓜分現(xiàn)金大獎(jiǎng)總結(jié)
以上是生活随笔為你收集整理的c++ 多重背包状态转移方程_动态规划入门——详解经典问题零一背包的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 焦作治疗子宫发育不良最好的医院推荐
- 下一篇: c++保存图标到dll_自动保存邮件附件