动态规划—01背包问题
原文作者:弗蘭克的貓?
原文地址:【動態規劃】01背包問題
摘要:
01背包問題:n個物品放入容量為c的背包中。
常見解法:
- 分治法:遞歸計算,且存在重復計算的bug
- 自上而下填表法:從大到小使用遞歸計算,利用二維數組保存已經計算過的結果,減少遞歸次數
- 自下而上填表法:從小到大使用轉移方程計算,利用二維數組保存所有結果。無遞歸,效率高
目錄
說明
最優化原理
無后效性
01背包問題
問題分析
分治法
動態規劃解法
驗證可行性
自上而下記憶法
自下而上填表法
總結
0|1說明
前面用動態規劃解決了正則表達式的問題,感覺還是不過癮,總覺得對于動態規劃的理解還沒有到位,所以趁熱打鐵,繼續研究幾個動態規劃的經典問題,希望能夠借此加深對動態規劃的理解。在此之前,還需要說兩個跟動態規劃有關的理論知識。
0|1最優化原理
最優化原理指的最優策略具有這樣的性質:不論過去狀態和決策如何,對前面的決策所形成的狀態而言,余下的諸決策必須構成最優策略。簡單來說就是一個最優策略的子策略也是必須是最優的,而所有子問題的局部最優解將導致整個問題的全局最優。如果一個問題能滿足最優化原理,就稱其具有最優子結構性質。這是判斷問題能否使用動態規劃解決的先決條件,如果一個問題不能滿足最優化原理,那么這個問題就不適合用動態規劃來求解。這樣說可能比較模糊,來舉個栗子吧:
如上圖,求從A點到E點的最短距離,那么子問題就是求從A點到E點之間的中間點到E點的最短距離,比如這里的B點。那么這個問題里,怎么證明最優化原理呢?我們假設從A點到E點的最短距離為d,其最優策略的子策略假設經過B點,記該策略中B點到E點的距離為d1,A點到B點的距離為d2。我們可以使用反證法,假設存在B點到E點的最短距離d3,并且d3 < d1,那么?d3 + d2 < d1 + d2 = d,這與d是最短距離相矛盾,所以,d1是B點到E點的最短距離。為了增加理解,這里再舉一個反例:
圖中有四個點,A、B、C、D,相鄰兩點有兩條連線,代表兩條通道,d1,d2,d3,d4,d5,d6代表的是道路的長度,求A到D的所有通道中,總長度除以4得到的余數最小的路徑為最優路徑,求一條最優路徑。這里如果還是按照上面的思路去求解,就會誤入歧途了。按照之前的思路,A的最優取值應該可以由B的最優取值來確定,而B的最優取值為(3+5)mod 4 = 0。所以應該選d2和d6這兩條道路,而實際上,全局最優解是d4+d5+d6或者d1+d5+d3。所以這里子問題的最優解并不是原問題的最優解,即不滿足最優化原理。所以就不適合使用動態規劃來求解了。
0|1無后效性
無后效性指的是某狀態下決策的收益,只與狀態和決策相關,與到達該狀態的方式無關。某個階段的狀態一旦確定,則此后過程的演變不再受此前各種狀態及決策的影響。換句話說,未來與過去無關,當前狀態是此前歷史狀態的完整總結,此前歷史決策只能通過影響當前的狀態來影響未來的演變。再換句話說,過去做的選擇不會影響現在能做的最優選擇,現在能做的最優選擇只與當前的狀態有關,與經過如何復雜的決策到達該狀態的方式無關。這也是用來驗證問題是否可以使用動態規劃來解答的重要方法。
我們再回頭看看上面的最短路徑問題,如果在原來的基礎上加上一個限制條件:同一個格子只能通過一次。那么, 這個題就不符合無后效性了,因為前一個子問題的解會對后面子問題的選擇策略有影響,比如說,如果從A到B選擇了一條如下圖中綠色表示的路線,那么從B點出發到達E點的路線就只有一條了。也就是說從A點到B點的路徑選擇會影響B點到E點的路徑選擇。
理論部分就此打住,接下來我們實戰一下。
0|101背包問題
假設你是一名經驗豐富的探險家,背著背包來到野外進行日常探險。天氣晴朗而不燥熱,山間的風夾雜著花香,正當你欣賞這世外桃源般的美景時,突然,你發現了一個洞穴,這個洞穴外表看起來其貌不揚,但憑借著驚為天人的直覺,這個洞穴不簡單。
于是,你開始往洞穴內探索,希望能發現一些有意思的東西。終于,皇天不負有心人,你在洞穴的盡頭,發現了一堆不世出的珠寶,憑借你驚人的閱歷,一眼便看出了它們各自的價值,心想著下下下下下下下下半輩子都有著落了。
然而,天有不測風云,正準備將它們收入囊中,卻不小心觸碰到一個防御機關,洞穴馬上就要崩塌了。在此危機時刻,你只有一個背包,你必須盡快做出抉擇,從中選擇最值錢的珠寶塞到你的背包,讓背包中珠寶的總價值最大。
好了好了,啰里啰嗦了大半天,我還是來精簡一下問題吧。簡而言之,你只有一個容量有限的背包,總容量為c,有n個可待選擇的物品,每個物品只有一件,它們都有各自的重量和價值,你需要從中選擇合適的組合來使得你背包中的物品總價值最大。
0|1問題分析
那還不簡單,不管是什么,先往背包里塞,塞滿趕緊走,狗命要緊,狗命要緊。。。
好了好了,開個玩笑,言歸正傳。簡單起見,我們來將上面的問題具體化,舉一個更具體的栗子:假設有4個物品,它們的價值(v)和重量(w)如下圖:
背包總容量為10,現在要從中選擇物品裝入背包中,要求物品的重量不能超過背包的容量,并且最后放在背包中物品的總價值最大。emmm,等等,為什么叫做0/1背包呢?為什么不叫1/2背包,2/3背包???仔細想想,這里每個物品只有一個,對于每個物品而言,只有兩種選擇,盤它或者不盤,盤它記為1,不盤記為0,我們不能將物品進行分割,比如只拿半個是不允許的。這就是這個問題被稱為0/1背包問題的原因。所以究竟選還是不選,這是個問題。
讓我們先來體驗一下將珠寶裝入背包的感覺,為了方便起見,用xi代表第i個珠寶的選擇(xi = 1?代表選擇該珠寶,0則代表不選),vi代表第i個珠寶的價值,wi代表第i個珠寶的重量。于是我們就有了這樣的限制條件:
我們的初始狀態是背包容量為10,背包內物品總價值為0,接下來,我們就要開始做選擇了。對于1號珠寶,當前容量為10,容納它的重量2綽綽有余,因此有兩種選擇,選它或者不選。我們選擇一個珠寶的時候,背包的容量會減少,但是里面的物品總價值會增加。就像下面這樣:
這樣就分出了兩種情況,我們繼續進行選擇,如果我們選擇了珠寶1,那么對于珠寶2,當前剩余容量為8,大于珠寶2的容量3,因此也有兩種選擇,選或者不選。
現在,我們得到了四個可能結果,我們每做出一個選擇,就會將上面的每一種可能分裂成兩種可能,后續的選擇也是如此,最終,我們會得到如下的一張決策圖:
這里被涂上色的方框代表我們的最終待選結果,本來應該有16個待選結果,但有三個結果由于容量不足以容納下最后一個珠寶,所以就沒有繼續進行裂變。然后,我們從這些結果中,找出價值最大的那個,也就是13,這就是我們的最優選擇,根據這個選擇,依次找到它的所有路徑,便可以知道該選哪幾個珠寶,最終結果是:珠寶4,珠寶2,珠寶1。
0|1分治法
接下來,我們就來分析一下,如何將它擴展到一般情況。為了實現這個目的,我們需要將問題進行抽象并建模,然后將其劃分為更小的子問題,找出遞推關系式,這是分治思想中很重要的一步。
那這里的遞推關系式是怎樣的呢?對于第i個物品,有兩種可能:
對于這個問題的子問題,這里有必要詳細說明一下。原問題是,將n件物品放入容量為c的背包,子問題則是,將前i件物品放入容量為j的背包,所得到的最優價值為KS(i,j),如果只考慮第i件物品放還是不放,那么就可以轉化為一個只涉及到前i-1個物品的問題。如果不放第i個物品,那么問題就轉化為“前i-1件物品放入容量為j的背包中的最優價值組合”,對應的值為KS(i-1,j)。如果放第i個物品,那么問題就轉化成了“前i-1件物品放入容量為j-wi的背包中的最優價值組合”,此時對應的值為KS(i-1,j-wi)+vi。所以,就可以很容易的寫出遞歸解法了:
public class Solution{int[] vs = {0,2,4,3,7};int[] ws = {0,2,3,5,5};@Testpublic void testKnapsack1() {int result = ks(4,10);System.out.println(result);}private int ks(int i, int c){int result = 0;if (i == 0 || c == 0){// 初始條件result = 0;} else if(ws[i] > c){// 裝不下該珠寶result = ks(i-1, c);} else {// 可以裝下int tmp1 = ks(i-1, c);int tmp2 = ks(i-1, c-ws[i]) + vs[i];result = Math.max(tmp1, tmp2);}return result;} }這里為了方便處理,將數組ws和vs都增加了一個補位數0,防止數組越界,輸出結果:
13這樣,我們就輕松加愉快的解決了這個問題。但是Math.max(ks(i-1, c),ks(i-1, c-ws[i]) + vs[i])遞歸計算的時候,會導致重復計算,如下圖所示:
0|1動態規劃解法
驗證可行性
既然開頭已經說了兩個驗證問題是否可以使用動態規劃求解的方法,那么為何不試一試呢?先來看看最優化原理。同樣,我們使用反證法:假設(x1,x2,…,xn)是01背包問題的最優解,則有(x2,x3,…,xn)是其子問題的最優解,假設(y2,y3,…,yn)是上述問題的子問題最優解,則有(v2y2+v3y3+…+vnyn)+v1x1 > (v2x2+v3x3+…+vnxn)+v1x1。說明(X1,Y2,Y3,…,Yn)才是該01背包問題的最優解,這與最開始的假設(X1,X2,…,Xn)是01背包問題的最優解相矛盾,故01背包問題滿足最優性原理。
至于無后效性,其實比較好理解。對于任意一個階段,只要背包剩余容量和可選物品是一樣的,那么我們能做出的現階段的最優選擇必定是一樣的,是不受之前選擇了什么物品所影響的。即滿足無后效性。
自上而下記憶法
就像上一篇里的解法一樣,自上而下的解法與分治法的區別就是增加了一個數組用來存儲計算的中間結果來減少重復計算。這里,我們只需要多定義一個二維數組。
表格中,每一個格子都代表著一個子問題,我們最終的問題是求最右下角的格子的值,也就是i=4,j=10時的值。這里,我們的初始條件便是i=0或者j=0時對應的ks值為0,這很好理解,如果可選物品為0,或者剩余容量為0,那么最大價值自然也是0。代碼如下:
public class Solution{int[] vs = {0,2,4,3,7};int[] ws = {0,2,3,5,5};Integer[][] results = new Integer[5][11];@Testpublic void testKnapsack2() {int result = ks2(4,10);System.out.println(result);}private int ks2(int i, int c){int result = 0;// 如果該結果已經被計算,那么直接返回//通過這行代碼解決分治法中重復計算的問題if (results[i][c] != null) return results[i][c];if (i == 0 || c == 0){// 初始條件result = 0;} else if(ws[i] > c){// 裝不下該珠寶result = ks(i-1, c);} else {// 可以裝下int tmp1 = ks(i-1, c);int tmp2 = ks(i-1, c-ws[i]) + vs[i];result = Math.max(tmp1, tmp2);results[i][c] = result;}return result;} }可以看到,其實只比分治多了三行代碼。而且通過數組保存以前計算結果的方式解決分治法中重復計算的問題
自下而上填表法
自上而下填表需要用到遞歸,效率較低,接下來,我們用自下而上的方法來解一下這道題,思路很簡單,就是不斷的填表,回想一下上一篇中的斐波拉契數列的自下而上解法,這里將使用同樣的方式來解決。還是使用上面的表格,我們開始一行行填表。
當i=1時,即只有珠寶1可供選擇,那么如果容量足夠的話,最大價值自然就是珠寶1的價值了。
當i=2時,有兩個物品可供選擇,此時應用上面的遞推關系式進行判斷即可。這里以i=2,j=3為例進行分析:
剩下的格子使用相同的方法進行填充即可:
這樣,我們就得到了最后的結果:13。根據結果,我們可以反向找出各個物品的選擇,尋找的方法很簡單,就是從i=4,j=10開始尋找,如果ks(i-1,j)=ks(i,j),說明第i個物品沒有被選中,從ks(i-1,j)繼續尋找。否則,表示第i個物品已被選中,則從ks(i-1,j-wi)開始尋找。
?
轉化成代碼:
public class Solution{int[] vs = {0,2,4,3,7};int[] ws = {0,2,3,5,5};Integer[][] results = new Integer[5][11];@Testpublic void testKnapsack3() {int result = ks3(4,10);System.out.println(result);}private int ks3(int i, int j){// 初始化for (int m = 0; m <= i; m++){results[m][0] = 0;}for (int m = 0; m <= j; m++){results[0][m] = 0;}// 開始填表for (int m = 1; m <= i; m++){for (int n = 1; n <= j; n++){if (n < ws[m]){// 裝不進去results[m][n] = results[m-1][n];} else {// 容量足夠if (results[m-1][n] > results[m-1][n-ws[m]] + vs[m]){// 不裝該珠寶,最優價值更大results[m][n] = results[m-1][n];} else {results[m][n] = results[m-1][n-ws[m]] + vs[m];}}}}return results[i][j];} }嗯,完美解決。時間復雜度即填表耗時O(n * c),這里用了一個二維數組來存儲子問題的解,所以空間復雜度為O(n * c);
0|1總結
回過頭再看看上面的分析,會發現動態規劃里最關鍵的問題其實是尋找原問題的子問題,并寫出遞推表達式,只要完成了這一步,代碼部分都是水到渠成的事情了。那么問題來了,怎樣把問題拆分成子問題呢?emmm,這個問題有點超綱了,說實話,我也沒有掌握到訣竅,還是得具體情況具體分析,但是很多經典的問題都有其經典的套路,其它問題都可以歸結到這些問題上面來,可以看做是它們的變種和延伸,把這些經典的問題吃透的話,自然能舉一反三。比如采藥問題,本質上就是01背包問題,而硬幣問題,本質上就是我們之后要介紹的完全背包問題。個人認為,算法不在于刷多少個,而在于歸納總結,就跟做數學題一樣,總有一些范式和套路,不管形式如何變化,其本質是一樣的,萬變不離其宗,說的就是這么回事。
本篇到此就告一段落了,如果覺得有收獲,不要吝嗇你的贊哦,也歡迎關注我的公眾號留言交流。
__EOF__
作 者:弗蘭克的貓
出 處:https://www.cnblogs.com/mfrank/p/10533701.html
關于博主:編程路上的小學生,熱愛技術,喜歡專研。評論和私信會在第一時間回復。或者直接私信我。
版權聲明:署名 - 非商業性使用 - 禁止演繹,協議普通文本?|?協議法律文本。
聲援博主:如果您覺得文章對您有幫助,可以點擊文章右下角【推薦】一下。您的鼓勵是博主的最大動力!
總結
以上是生活随笔為你收集整理的动态规划—01背包问题的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 单元测试之—JUnit与SpringTe
- 下一篇: 动态规划—完全背包问题