本人不才,雖然學過運籌學,但早已忘了個干凈,最近在看面試題,發現DP問題非常普遍,而我總喜歡用遞歸搜索的思維方式去解決DP問題,做了幾道題才發現自己根本沒有理解DP,也沒有使用DP。所以在看到01背包問題的解的時候百思不得其解。相信不少人跟我一樣也會對01問題的遞推公式不理解,下面就通過一道程序的打印結果來分析一下到底DP在程序中是如何運作的。
設總的背包大小是V,申請一個大小是V的數組f[V]來存儲中間結果。
下面要開始發揮想象力了:
想象一下,現在你有V個從1到V容量連續變化的V個背包(而不是只有一個背包),
第一步迭代:
不論目前手邊有多少個物件,首先拿起第一個物件1,在手里掂量掂量,看起來可以放進背包V中,於是記錄一下:“如果手邊只有物件1,那么容量為V的背包能放的最大價值是物件1的價值W[1]”,然后再掂量掂量物件1,發現還可以放在V-1容量的背包里,再記錄一下:“如果手邊只有物件1,那么容量為V-1的背包能放的最大價值是物件1的價值W[1]”......如此直到拿出容量為V-C[1](C[1]是物件1的體積)的那個背包時,發現這個背包的容積剛好是物件1的體積,那么比這個背包容量再小的背包放不下物件1了,於是記錄一下:“矮油,如果手邊只有物件1,那么容量小於V-C[1]的那些背包里面什么都放不了,最大價值也只能是0了”。得到的f[V]的結果是:0,0,...,W[1],W[1],W[1]。
第二步迭代:
現在拿出第二個物品,假設這個物品的價值比第一個物品大,體積也更大。到底要不要把物件2放入背包中呢?如果背包V能同時放下兩個物品,那么就都放下最好,價值達到W[1]+W[2],但當某個容積小於V的背包不能同時放下兩個物品時,就會出現一個抉擇問題,要放物件1還是物件2?那就得比較物件1和物件2的價值,哪個大就放哪個。當容積繼續變小,沒法放下物件2的時候,那么就只有放物件1了。得到f[V]的結果是:0,0,...,W[1],W[1],...,W[2],w[2],....,W[1]+W[2]。
根據“1生2,2生3,3生無窮的思想”進行
第三步迭代:
假設物件3的價值大於物件1和物件2,拿出背包V,發現背包同時放不下三個物件,這個時候,到底要不要把物件3放到這個背包里面呢?好了,最關鍵的時候到了。拿出容積為V的最大的那個背包,再拿出容積和最大的背包相差正好為物件3體積的背包,也就是說,最大的背包里面放着一個小背包,再加上物件3,如果這個小背包裝滿了的話,則正好可以裝滿背包V(不過小背包是否裝滿並不重要)。這個時候,小背包中不放物件3的最大價值是已知的,最大的背包V如果不放物件3的最大價值也是已知的,什么時候要選擇放入小背包和物件3呢?只有當小背包加物件3的價值比最大的背包V不放物件3的最大價值還大的時候,才會選擇將小背包和物件3放入這個大背包。
解釋到這里,不上代碼說不過去了,抄來的代碼,自己寫的注釋:
#define EMPTY #include <iostream> using namespace std ; const int V = 10 ; //總的體積 const int T = 5 ; //物品的種類個數 int f[V+1] ; int w[T] = {8 , 10 , 9 , 5 , 5}; //價值 int c[T] = {3 , 6 , 5 , 2 , 3}; //每一個的體積 const int INF = -66536 ; int package() { #ifdef EMPTY for(int i = 0 ; i <= V ;i++) //條件編譯,表示背包可以不存儲滿 f[i] = 0 ; #else f[0] = 0 ; for(int i = 1 ; i <= V ;i++)//條件編譯,表示背包必須全部存儲滿 f[i] = INF ; #endif for(int i = 0 ; i < T ; i++) { for(int v = V ; v >= c[i] ;v--) //不用非要減到0,本身總容量就放不下物件i的背包就不用進行決策了 { f[v] = max(f[v-c[i]] + w[i] , f[v]) ; //小背包的容積是大背包v減去要放的物件i的體積v-c[i],物件i的價值w[i]。則沒放物件3的小背包的價值為f[v-c[i]],加上物件i的總價值是f[v-c[i]] + w[i],如果這個值比不放物件i的大背包最大價值大,那么就把物件i放入背包v。 } for (int j=0;j<V;j++) { printf("%d,",f[j]); } printf("\n"); } return f[V] ; } int main() { int temp = package() ; cout<<temp<<endl ; system("pause") ; return 0 ; }
算法精妙之處在於把背包容積作為了數組的下標,通過背包容積就能找到這個背包的最大價值,然后決定是否要將當前物件放入大背包中。DP的精髓在於一個問題有完整的子結構(而不是子問題!!),求解最終答案的過程中需要用到子結構的決策結果來做下一輪的決策。可以發現,為了求最終容積為V的背包最優容量,把從0到V-1容量背包的問題全都求解了,當然這是必須的,否則無法得出背包V的解。找出了DP問題的狀態轉移方程,基本都能用這個代碼框架求解,狀態轉移方程相當重要!!
之前我一直把子結構和子問題混淆,所以總是往遞歸搜索的思路上走,最后越走越糊塗,弄了兩天終於搞明白了,特此記之!!