動態規劃解決01揹包問題
一、問題描述:有n 個物品,它們有各自的重量和價值,現有給定容量的揹包,如何讓揹包裡裝入的物品具有最大的價值總和?
二、總體思路:根據動態規劃解題步驟(問題抽象化、建立模型、尋找約束條件、判斷是否滿足最優性原理、找大問題與小問題的遞推關係式、填表、尋找解組成)找出01揹包問題的最優解以及解組成,然後編寫程式碼實現;
三、動態規劃的原理及過程:
eg:number=4,capacity=8
i |
1 |
2 |
3 |
4 |
w(體積) |
2 |
3 |
4 |
5 |
v(價值) |
3 |
4 |
5 |
6 |
1、原理
動態規劃與分治法類似,都是把大問題拆分成小問題,通過尋找大問題與小問題的遞推關係,解決一個個小問題,最終達到解決原問題的效果。但不同的是,分治法在子問題和子子問題等上被重複計算了很多次,而動態規劃則具有記憶性,通過填寫表把所有已經解決的子問題答案紀錄下來,在新問題裡需要用到的子問題可以直接提取,避免了重複計算,從而節約了時間,所以在問題滿足最優性原理之後,用動態規劃解決問題的核心就在於填表,表填寫完畢,最優解也就找到。
2、過程
a) 把揹包問題抽象化(X1,X2,…,Xn,其中 Xi 取0或1,表示第 i 個物品選或不選),Vi表示第 i 個物品的價值,Wi表示第 i 個物品的體積(重量);
b) 建立模型,即求max(V1X1+V2X2+…+VnXn);
c) 約束條件,W1X1+W2X2+…+WnXn<capacity;
d) 定義V(i,j):當前揹包容量 j,前 i 個物品最佳組合對應的價值;
e) 最優性原理是動態規劃的基礎,最優性原理是指“多階段決策過程的最優決策序列具有這樣的性質:不論初始狀態和初始決策如何,對於前面決策所造成的某一狀態而言,其後各階段的決策序列必須構成最優策略”。判斷該問題是否滿足最優性原理,採用反證法證明:
假設(X1,X2,…,Xn)是01揹包問題的最優解,則有(X2,X3,…,Xn)是其子問題的最優解,
假設(Y2,Y3,…,Yn)是上述問題的子問題最優解,則理應有(V2Y2+V3Y3+…+VnYn)+V1X1 > (V2X2+V3X3+…+VnXn)+V1X1;
而(V2X2+V3X3+…+VnXn)+V1X1=(V1X1+V2X2+…+VnXn),則有(V2Y2+V3Y3+…+VnYn)+V1X1 > (V1X1+V2X2+…+VnXn);
該式子說明(X1,Y2,Y3,…,Yn)才是該01揹包問題的最優解,這與最開始的假設(X1,X2,…,Xn)是01揹包問題的最優解相矛盾,故
f) 尋找遞推關係式,面對當前商品有兩種可能性:
第一,包的容量比該商品體積小,裝不下,此時的價值與前i-1個的價值是一樣的,即V(i,j)=V(i-1,j);
第二,還有足夠的容量可以裝該商品,但裝了也不一定達到當前最優價值,所以在裝與不裝之間選擇最優的一個,即V(i,j)=max{ V(i-1,j),V(i-1,j-w(i))+v(i) }
其中V(i-1,j)表示不裝,V(i-1,j-w(i))+v(i) 表示裝了第i個商品,揹包容量減少w(i)但價值增加了v(i);
由此可以得出遞推關係式:
1) j<w(i) V(i,j)=V(i-1,j)
2) j>=w(i) V(i,j)=max{ V(i-1,j),V(i-1,j-w(i))+v(i) }
g) 填表,首先初始化邊界條件,V(0,j)=V(i,0)=0;
h) 然後一行一行的填表,
1) 如,i=1,j=1,w(1)=2,v(1)=3,有j<w(1),故V(1,1)=V(1-1,1)=0;
2) 又如i=1,j=2,w(1)=2,v(1)=3,有j=w(1),故V(1,2)=max{ V(1-1,2),V(1-1,2-w(1))+v(1) }=max{0,0+3}=3;
3) 如此下去,填到最後一個,i=4,j=8,w(4)=5,v(4)=6,有j>w(4),故V(4,8)=max{ V(4-1,8),V(4-1,8-w(4))+v(4) }=max{9,4+6}=10;所以填完表如下圖:
1 void FindMax()//動態規劃 2 { 3 int i,j; 4 //填表 5 for(i=1;i<=number;i++) 6 { 7 for(j=1;j<=capacity;j++) 8 { 9 if(j<w[i])//包裝不進 10 { 11 V[i][j]=V[i-1][j]; 12 } 13 else//能裝 14 { 15 if(V[i-1][j]>V[i-1][j-w[i]]+v[i])//不裝價值大 16 { 17 V[i][j]=V[i-1][j]; 18 } 19 else//前i-1個物品的最優解與第i個物品的價值之和更大 20 { 21 V[i][j]=V[i-1][j-w[i]]+v[i]; 22 } 23 } 24 } 25 } 26 }
i) 表格填完,最優解即是V(number,capacity)=V(4,8)=10,但還不知道解由哪些商品組成,故要根據最優解回溯找出解的組成,根據填表的原理可以有如下的尋解方式:
1) V(i,j)=V(i-1,j)時,說明沒有選擇第i 個商品,則回到V(i-1,j);
2) V(i,j)=V(i-1,j-w(i))+v(i)實時,說明裝了第i個商品,該商品是最優解組成的一部分,隨後我們得回到裝該商品之前,即回到V(i-1,j-w(i));
3) 一直遍歷到i=0結束為止,所有解的組成都會找到。
j) 如上例子,
1) 最優解為V(4,8)=10,而V(4,8)!=V(3,8)卻有V(4,8)=V(3,8-w(4))+v(4)=V(3,3)+6=4+6=10,所以第4件商品被選中,並且回到V(3,8-w(4))=V(3,3);
2) 有V(3,3)=V(2,3)=4,所以第3件商品沒被選擇,回到V(2,3);
3) 而V(2,3)!=V(1,3)卻有V(2,3)=V(1,3-w(2))+v(2)=V(1,0)+4=0+4=4,所以第2件商品被選中,並且回到V(1,3-w(2))=V(1,0);
4) 有V(1,0)=V(0,0)=0,所以第1件商品沒被選擇;
k) 到此,01揹包問題已經解決,利用動態規劃解決此問題的效率即是填寫此張表的效率,所以動態規劃的時間效率為O(number*capacity)=O(n*c),由於用到二維陣列儲存子問題的解,所以動態規劃的空間效率為O(n*c);
1 void FindWhat(int i,int j)//尋找解的組成方式 2 { 3 if(i>=0) 4 { 5 if(V[i][j]==V[i-1][j])//相等說明沒裝 6 { 7 item[i]=0;//全域性變數,標記未被選中 8 FindWhat(i-1,j); 9 } 10 else if( j-w[i]>=0 && V[i][j]==V[i-1][j-w[i]]+v[i] ) 11 { 12 item[i]=1;//標記已被選中 13 FindWhat(i-1,j-w[i]);//回到裝包之前的位置 14 } 15 } 16 }
3、空間優化
l) 空間優化,每一次V(i)(j)改變的值只與V(i-1)(x) {x:1...j}有關,V(i-1)(x)是前一次i迴圈儲存下來的值;
因此,可以將V縮減成一維陣列,從而達到優化空間的目的,狀態轉移方程轉換為 B(j)= max{B(j), B(j-w(i))+v(i)};
並且,狀態轉移方程,每一次推導V(i)(j)是通過V(i-1)(j-w(i))來推導的,所以一維陣列中j的掃描順序應該從大到小(capacity到0),否者前一次迴圈儲存下來的值將會被修改,從而造成錯誤。
m) 同樣以上述例子中i=3時來說明,有:
1) i=3,j=8,w(3)=4,v(3)=5,有j>w(3),則B(8)=max{B(8),B(8-w(3))+v(3)}=max{B(8),B(4)+5}=max{7,4+5}=9;
2) j- -即j=7,有j>w(3),則B(7)=max{B(7),B(7-w(3))+v(3)}=max{B(7),B(3)+5}=max{7,4+5}=9;
3) j- -即j=6,有j>w(3),則B(6)=max{B(6),B(6-w(3))+v(3)}=max{B(6),B(2)+5}=max{7,3+5}=8;
4) j- -即j=5,有j>w(3),則B(5)=max{B(5),B(5-w(3))+v(3)}=max{B(5),B(1)+5}=max{7,0+5}=7;
5) j- -即j=4,有j=w(3),則B(4)=max{B(4),B(4-w(3))+v(3)}=max{B(4),B(0)+5}=max{4,0+5}=5;
6) j- -即j=3,有j<w(3),繼續訪問陣列會出現越界,所以本輪操作停止,B(0)到B(3)的值保留上輪迴圈(i=2時)的值不變,進入下一輪迴圈i++;
如果j不逆序而採用正序j=0...capacity,如上圖所示,當j=8時應該有B(8)=B(8-w(3))+v(3)=B(4)+5,然而此時的B(4)已經在j=4的時候被修改過了,原來的B(4)=4,現在B(4)=5,所以計算得出B(8)=5+5=10,顯然這於正確答案不符合;所以該一維陣列後面的值需要前面的值進行運算再改動,如果正序便利,則前面的值將有可能被修改掉從而造成後面資料的錯誤;相反如果逆序遍歷,先修改後面的資料再修改前面的資料,此種情況就不會出錯了;
1 void FindMaxBetter()//優化空間後的動態規劃 2 { 3 int i,j; 4 for(i=1;i<=number;i++) 5 { 6 for(j=capacity;j>=0;j--) 7 { 8 if(B[j]<=B[j-w[i]]+v[i] && j-w[i]>=0 )//二維變一維 9 { 10 B[j]=B[j-w[i]]+v[i]; 11 } 12 } 13 } 14 }
n) 然而不足的是,雖然優化了動態規劃的空間,但是該方法不能找到最優解的解組成,因為動態規劃尋早解組成一定得在確定了最優解的前提下再往回找解的構成,而優化後的動態規劃只用了一維陣列,之前的資料已經被覆蓋掉,所以沒辦法尋找,所以兩種方法各有其優點。
四、蠻力法檢驗:
1) 蠻力法是解決01揹包問題最簡單最容易的方法,但是效率很低
2) (X1,X2,…,Xn)其中Xi=0或1表示第i件商品選或不選,共有n(n-1)/2種可能;
3) 最簡單的方式就是把所有拿商品的方式都列出來,最後再做判斷此方法是否滿足裝包條件,並且通過比較和記錄找出最優解和解組成(如果滿足則記錄此時的價值和裝的方式,當下一次的裝法優於這次,則更新記錄,如此下去到最後便會找到最優解,同時解組成也找到);
4) n件商品,共有n(n-1)/2種可能,故蠻力法的效率是指數級別的,可見效率很低;
5) 蠻力法效率低不建議採取,但可以用於檢驗小規模的動態規劃解揹包問題的正確性和可行性,如下圖輸出可見,解01揹包問題用動態規劃是可行的:
五、總結:
對於01揹包問題,用蠻力法與用動態規劃解決得到的最優解和解組成是一致的,所以動態規劃解決此類問題是可行的。動態規劃效率為線性,蠻力法效率為指數型,結合以上內容和理論知識可以得出,解決此問題用動態規劃比用蠻力法適合得多。對於動態規劃不足的是空間開銷大,資料的儲存得用到二維陣列;好的是,當前問題的解只與上一層的子問題的解相關,所以,可以把動態規劃的空間進行優化,使得空間效率從O(n*c)轉化為O(c),遺憾的是,雖然優化了空間,但優化後只能求出最優解,解組成的探索方式在該方法執行的時候已經被破壞掉;總之動態規劃和優化後的動態規劃各有優缺點,可以根據實際問題的需求選擇不同的方式。
六、引申:
動態規劃可以解決哪些型別的問題?
待解決的原問題較難,但此問題可以被不斷拆分成一個個小問題,而小問題的解是非常容易獲得的;如果單單只是利用遞迴的方法來解決原問題,那麼採用的是分治法的思想,動態規劃具有記憶性,將子問題的解都記錄下來,以免在遞迴的過程中重複計算,從而減少了計算量。