動態規劃揹包問題——01揹包(超詳細)

01揹包問題

問題背景:
設有容量爲V的揹包,有N種物品,每件物品的體積是c[i],對應的價值是v[i],每種物品只有一個,要使揹包中所裝物品的價值總和最大,問該如何選擇。
用一個二維數組f[i][j],來表示i件物品,在用j的容量時的最大的價值。因爲對於每一件物品,你選擇就只能是裝或者不裝這件物品。
例如:
物品: a b c d
體積: 5 4 6 10
價值; 10 7 14 25
當你揹包裝1件物品時,假如你的揹包容量爲4,此時f[1][4]=7,你只能裝物品b,如果你揹包容量爲9,這時你就有了選擇,能夠裝下物品a,b,c中的任何一個,因爲只能裝下一件物品,所以即使你能裝下a和b,你當然也只能選價值最大的物品c,即f[1][9]=14。
第一件物品當然是比較簡單,當揹包裝兩件物品時(i=2),當你揹包還能裝下第二件物品,此時你有兩個選擇,對第二件物品拿與不拿:
拿的話就是f[i][j] = f[i-1][j – c[i]] + v[i] 下劃線部分是拿第i個物品之前最多能拿的價值,再加上v[i](第i個物品的價值)這就是拿第i個物品時的最大價值。
不拿的話就是f[i][j] = f[i-1][j]就相當於裝i-1件物品的最大價值。當你揹包裝不下第二件物品的時候也是f[i][j] = f[i-1][j](裝不下了,還不是隻能像前面一樣唄)
這時,我們每一步都是取的最佳方案(得到了最大價值)
我們就可以得出狀態轉移方程 f[i][j] = max(f[i-1][j], f[i-1][j – c[i]] + v[i])
目標就是f[N][V]

這裏有個難理解的地方就是初學者可能認爲,f[i][j] = f[i-1][j – c[i]] + v[i]不是裝得下就裝了嗎?
並不是這樣!!! 當你的揹包容量V = 26時
我們不如打個表看看在這裏插入圖片描述

比如f[3][25] = max(f[2][25], f[2][25-6] + 14) = max(39, 56)=56 這就不對了 爲什麼呢?!
比如f[4][25] = max(f[3][25], f[3][25-10] + 25) = max(49,56)=56; 這又是對的
但是!!! 程序出來是這樣的在這裏插入圖片描述

在這裏
比如f[3][25] = max(f[2][25], f[2][25-6] + 14) = max(17, 31)=31
比如f[4][25] = max(f[3][25], f[3][25-10] + 25) = max(31,56)=56;
這纔是正解(爲什麼會這樣的原因,相信讀者能自己分析出來)
二維數組的時間和空間複雜度都是O(VN)
有沒有發現,只有最後一行代碼纔有用,這時我們想到了用一維數組來優化。
時間複雜度不能再進行優化了,但可以將空間複雜度優化。

先寫出狀態轉移方程f[j] = max(f[j], f[j – c[i]] + v[i])

我們可以繼續分析一波數據
還是用上面的例子:
物品: a b c
體積: 5 4 6
價值; 10 7 14

當 i = 1 時
f[0] 到 f[4]顯然都是零
f[5] = max(f[5], f[0] + 10)= 10
f[6] = max(f[6], f[1] + 10) = 10

f[9] = max(f[9], f[4] + 10) = 10
f[10] = max(f[10], f[5] + 10)=20
!!! 問題出現了,如果每件物品只拿一次,價值不可能出現20
仔細分析發現原來是物品a拿了兩次既f[10]不僅由f[5]決定了而且還被f[0]影響了,這顯然是不對的,這就是我們後面要講的完全揹包問題。
我們想出了一個解決辦法,不是f[10]被前面多次刷新嘛,我們第二層循環倒着來。J=14開始(我們不分析這麼多了揹包就取14)到j>=c[i]就可以了,因爲前面都是0,沒有意義。
i = 1時
f[14] = max(f[14], f[9] + 10) = 10
f[13] = max(f[13], f[8] + 10) = 10

f[5] = max(f[5], f[0] + 10) = 10
i = 2時
f[14] = max(f[14], f[10] + 7) = 17

f[9] = max(f[9], f[5] + 7) = 17
f[8] = max(f[8], f[4] + 7) = 10;
看到沒,f[9]到f[14]的值刷新了
i = 3 時
f[14] = max(f[14, f[8] + 14] = 24
f[13] = max(f[13], f[7] +14) = 24

後面就不列舉了,我們要的結果f[14]已經出來了顯然這是正確的答案。
由此可見對於二維數組,我們可以用一維滾動數組來實現,減少不必要的浪費。
重點來了!!!用滾動數組的時候內層循環一定要逆序

再寫一點關於初始化的問題,對於揹包問題,有要求揹包裝滿與不裝滿兩種情況
  1. 裝滿那就要f[0] = 0; 其他的都爲-∞,爲什麼呢,因爲只有在揹包容量爲0的時候纔可以一個都不裝價值爲0,當揹包有容量,就可能裝得下東西,但你不能選擇什麼都不裝對吧,這不符合要求,所以初始化爲-∞之後,只有當你裝之後這個值纔有意義。
  2. 不要求裝滿就全部初始化爲0,理由同上,裝與不裝是你的選擇,你可以留着容量不裝,這時所有值都有意義。
    最後貼上01揹包的代碼
    二維:
for(int i = 1; i <= N; i++)//N表示種類
	for(int j = 1; j <= V; j++)//V表示揹包容量 這裏循環正序逆序都行 
	if(j >= c[i])
	f[i][j] = max(f[i-1][j], f[i-1][j - c[i]] + v[i]) 
	else
	f[i][j] = f[i-1][j]
	cout << f[N][V];

滾動數組:

for(int i = 1; i <= N; i++)
	for(int j = V; j >= c[i]; j--)// 一定要逆序 
	f[j] = max(f[j], f[j - c[i]] + v[i]);
	cout << f[V];
注:此文章問原創,開源,可轉載但請註明出處!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章