動規基礎——01背包問題(背包問題Ⅱ)


題目來源:領扣 | 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);

  1. 找到遞歸出口
    上文提到的,背包無剩余空間或者物品算完了,即(j0||i0),此時它返回的最大價值應該是0;
  2. 遞推關系
    這個關系有兩種,比較容易想到的是 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問題有 數字金字塔 等。動態規划太靈活了,沒有固定的模板,要根據問題具體分析

完整代碼參考我的GitHub


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM