題目來源:領扣 | LintCode
有 i 個物品和一個總容量為 j 的背包. 給定數組 weight 表示每個物品的重量和數組 value 表示每個物品的價值,求最大價值。(物品不能分割)
背包問題II
這道題是一道動態規划(dp)算法的基礎題,有兩種實現方式,分別是遞歸和遞推(迭代),前者比后者好理解。
解題思路
首先,題目的要求是找出最大價值,所以我們要想,怎么存放才能讓他的價值最大呢?因為物品具有重量,背包容量也有限,所以我們不能每次都放入最大價值的物品,舉個例子,假設背包容量為 12,現在有三個物品對應價值和重量的關系如下表
物品|質量|價值
A 10 8
B 6 5
C 5 4
顯然我們就要選B和C,然后有人可能就會靈光一閃,那全都選擇性價比(價值/質量)最大的不就行了(貪心算法),但是題目還有要求“物品不能分割”,所以實際上我們不能使用貪心算法。
我們可以從最普通的思考方式得到一個動態規划的遞推式子。我們有i個物品,考慮到重量的情況下,按照通常人類的想法就是找出各種組合,然后依次比較這些組合的價值,選最大的那個就行了,所以我們線性遍歷i個物品的時候就有兩種選擇,“要”和“不要”,
每個物品都經歷這兩個選擇,最終就是產生全部的組合方式。當輪到第i個物品的時候,我們的最大價值就是這樣一個式子:maxvalue = max(value[i],value[i-1]) (並考慮每次選擇背包能不能裝得下),意思就是要第i個物品和不要第i個物品那個情況得到的價值最大,因為判斷i個物品我們需要知道i-1個時的最大價值,所以這個式子會一直傳遞到背包滿了或者物品都確認完了,所以可以想到用遞歸來解決這個過程。
遞歸算法實現
我們需要讓程序知道每個物品的信息,因此需要兩個數組來儲存每個物品對應的信息(也可用一個結構體數組),分別是value[],weight[]。遞歸程序需要有會變化的參數,物品的價值和重量時不會變的,顯然會變的是物品的數量和背包的剩余容量,所以可以寫一個函數bag(前i個物品,剩余背包容量j);
- 找到遞歸出口
上文提到的,背包無剩余空間或者物品算完了,即(j0||i0),此時它返回的最大價值應該是0; - 遞推關系
這個關系有兩種,比較容易想到的是 maxvalue = max(bag(i-1,j),bag(i-1,j-weight[i])+value[i]) 前一個是“不要”,后一個是“要”;
還有一種情況是 當j!=0但是當前選擇的物品的重量weight[i]比剩下空間j多了,我們就要跳過這個物品,即強制選擇“不要”。
完成這兩部我們就能初步得到遞歸的算法啦!
int bag(int i, int j) //遞歸實現
{
if (i == 0 || j == 0)
return 0;
if (weight[i] > j)
return bag(i-1,j);
else {
int res = max(bag(i - 1, j), bag(i - 1, j - weight[i]) + value[i]);
return res;
}
//max函數可以自己判斷每一種情況下的最大價值(TIPS:對於遞歸程序不能刻意去思考它的過程,主要理解它的方向)
}
然而這個還不能稱為DP,因為這個效率極差,我們發現它計算幾百種情況,難免會出現重復的,重復的節點可能是物品為i,重量為j,所以可以建立一個用來記錄的二維數組maxdata[i][j],每個i,j都是一種情況,儲存這種情況下的最大價值,這樣效率就能大大提高
int bag(int i, int j) //遞歸實現
{
if (maxdata[i][j] != EOF) //一開始讓每個點都等於EOF(-1),如果不是-1證明這個情況已經算過了,可以直接return
return maxdata[i][j];
if (i == 0 || j == 0)
return 0;
if (weight[i] > j)
return bag(i-1,j);
else {
int res = max(bag(i - 1, j), bag(i - 1, j - weight[i]) + value[i]);
maxdata[i][j] = res; //沒算過就存一下
return res;
}
}
迭代算法
迭代算法和遞歸算法有一個本質的區別,遞歸我們是從i個物品一直找到剩下1個,而迭代算法則恰恰相反,要從一個開始找出最大價值,並逐步向上得到i個物品能得到的最大價值,而我們要記錄的是前i種物品在各種重量下的最大價值。(這是比較難理解的點)
但是具體的判斷方法仍然是“要”和“不要”的問題。因為是從1個開始,所以我們每輪遞推的關鍵點在於重量,遞推式還是那個樣子max(maxdata[j],max[j-weight[i]]+value[i]);但是我們是根據數組中儲存的前一輪的數據進行判斷的。
然后再來分析一下這個遞推式,假如現在正計算i種物品,maxdata[j]則是i-1種物品對應j重量的最大價值,如果我們“不要”第i個物品,那么最大價值還是i-1個物品對應的最大價值,如果我們“要”第i個物品,那么要對應前i-1個物品在容量扣除weight[i]時的最大價值,然后加上value[i]。這兩個數值哪個大就選哪個作為這一輪的最大價值,因為判斷完成后上一輪對應的數據就沒有用了 所以我們新的數據就可以覆蓋在對應的位置,即maxdata[j] = max(maxdata[j],maxdata[j-weight[i]]+value[i]);
核心代碼奉上
//遞推(迭代) 滾動數組
int f[100] = { 0 }; //f[j]儲存前一輪各個重量下最大價值
for (int i = 1; i <= n; i++) { //枚舉種類j
for (int j = totalweight; j >= 0; j--){ //算前n種的各個重量下最大價值
int next_w = j - weight[i]; //“要”這個物品的情況下對應的下標
if (next_w < 0) next_w = 0; //防止下標越界
if (weight[i] > j)data
f[j] = f[j]; //超重則最大價值等於前一輪對應容量下的最大價值,直接“不要”
else
f[j] = max(f[j], f[next_w] + value[i]);
if(i==n) break; //最后一個物品只需要算一個totalweight的就夠了
}
}//最終得到的f[totalweight]就是n個物品用整個背包去裝能得到的最大價值。
理解這個程序,只要搞懂i = 1 到 i = 2 的過程就能理解全部了。可以隨便舉個例子:假設背包容量為 12
編號|物品|質量|價值
1 A 10 8
2 B 6 5
3 C 5 4
- 從第1個A開始,我們發現容量大於等於10的時候最大價值就是8,其他的都是0,此時f[]內的數據全是零,於是大於等於10以后的式子都會是f[j] = max(0,0+8),所以就能得到新的f[];
f[] = {0 0 0 0 0 0 0 0 0 0 8 8 8 ……} - 然后在看第二個物品B,
在總容量為12時,有f[12] = max(f[12],f[12-6]+5) f[12]從前一輪得出等於8,f[6] = 0, 所以顯然在這個容量的限制下,我們會選擇“不要”。
以此類推,我們可以發現直到 5<j<10 我們才會選擇"要”,因此可以得到這一輪的f[] = {0 0 0 0 0 0 6 6 6 6 8 8 8……} - 如果還沒理解就,繼續看下第三個物品C
跟前一輪完全相同的想法,當j=12的時候,f[12] = max(f[12],f[12-5]+4) 我們發現 f[12] = 8 < f[7]+5 = 11; 所以我們選擇“要”
因為這是最后一個了所以繼續往下算已經沒用了,我們已經得到3個物品,12容量下的最大價值則為 11 ;
到這里迭代算法就算講解完了!!
迭代算法的流程圖
閑話
第一次寫這種博客,感覺還挺有趣的,最重要的是還能讓自己復習學過的知識,本篇內容是按自己的理解寫的,可能存在邏輯錯誤或者漏洞,如果發現問題還請評論指教。
動態規划的學習可以參考mooc上郭偉的《算法與程序設計》,最簡單的dp問題有 數字金字塔 等。動態規划太靈活了,沒有固定的模板,要根據問題具體分析