一 、01背包問題
題目
有N件物品和一個容量為V的背包。放入第i件物品耗費的空間是Ci,得到的價值是Wi。求解將哪些物品裝入背包可使價值總和最大。
思路
這是最基本的背包問題:每種物品僅有一件,可以選擇放或不放。
定義狀態:F[i,v]表示前i件物品恰放入一個容量為v的背包可以獲得的最大價值。則其狀態轉移方程便是:F[i,v]=max{F[i-1,v],F[i-1,v-Ci]+Wi}
這個方程很重要,這里來解釋一下:“將前i件物品放入容量為v的背包中”這個子問題,若只考慮第i件物品的策略(放還是不放),就可以轉化為一個只和前i-1個物品相關的問題。如果不放,則轉化為F[i,v];如果放,則為F[i-1,v-Ci]+Wi.
偽代碼如下:
F[0,0..V] = 0
for i = 1 to N
for v = Ci to V
F[i,v] = max{F[i-1,v],F[i-1,v-Ci]+Wi}
空間優化
將空間復雜度降為O(V),即只用一維數組。首先第一層循環一定是有的,每次計算出F[i,0..V]的所有值。由於i是遞增變化的,那么我們用一維數組F[0..V],每次循環時都能使用上一次循環的值,也就是i-1的數組值。接下來有個問題,怎么保證F[v]一定是由上一次的F[v]和F[v-Ci]轉化過來呢?
這要求我們在第二層循環時遵循從V到0的遞減順序計算F[v],這樣能保證計算F[v]時,F[v-Ci]保存的是上一次的值,即F[i-1,v-Ci]。可以這么理解,由狀態轉移方程我們知道,F[i,v]是由F[i-1,v]或F[i-1,v-Ci]轉化得到,現在只看第二維,每次計算新的F值時都是從第二維比較小的狀態過來的,如v和v-Ci,都是小於等於v的,那么只有讓第二層從大到小更新,才保證了F[i,v]是由F[i-1,v]或F[i-1,v-Ci]轉化得到;否則就成了F[i,v]由F[i,v-Ci]得到的,這是不符合最初的算式的。
偽代碼:
F[0,0..V] = 0
for i = 1 to N
for v = V to Ci
F[v] = max{F[v],F[v-Ci]+Wi}
總結
這樣我們可以抽象出一個處理一件01背包的物品過程。
ZeroOnePack(F,C,W)
for v = V to C
F[v] = max(F[v],F[v-C]+W)
有了上述函數,01背包的偽代碼就可以這樣寫:
F[0..V] = 0
for i = 1 to N
ZeroOnePack(F,Ci,Wi)
初始化問題
若問題要求剛好裝滿背包,初始化時,則除了F[0]=0,F[1..V]均為-inf(非法解)。
如果不要求裝滿,則初始化時F[0..V]=0。
二、完全背包問題
題目
有N種物品和一個容量為V的背包,每種物品都有無限件。放入第i件物品耗費的空間是Ci,得到的價值是Wi。求解將哪些物品裝入背包可使價值總和最大。
思路
這個問題於01背包相似,唯一不同的就是這里的物品有無限件。也就是其取法的策略不再是取或不取兩種了,而是取0件、取1件······直至取INT(V/Ci)件等多種。
但我們仍然可以采用解決01背包的思路來求解這個問題。
列出轉移方程:F[i,v]=max{F[i-1,v-k*Ci]+k*Wi | 0<=k*Ci<=v}
總復雜度為O( V*N*Σ(V/Ci) ),比較大。
優化
一個簡單有效的優化:若兩件物品i、j滿足Ci<=Cj且Wi>=Wj,則可以將物品j去掉,不用考慮。
轉化為01背包問題
把第i種物品拆分成費用為Ci*2^k、價值為Wi*2^k的若干件物品,其中k取滿Ci*2^k<=V的非負整數。這就是二進制的思想,因為不管最優策略選幾件,其件數總是能表示成若干的2^k件物品的和。這樣就把每種物品拆成INT(log(V/Ci))件物品了。
O(V*N)的算法
這個算法采用一維數值,先上偽代碼:
F[0,0..V] = 0
for i = 1 to N
for v = Ci to V
F[v] = max{F[v],F[v-Ci]+Wi}
這個偽代碼與01背包的偽代碼只有v的循環次序不同而已。
為什么這個算法要以這樣的次序呢?首先想想為什么01背包的中要按照v遞減的次序來循環。讓v遞減是為了保證第i次循環中的狀態F[i,v]是由狀態F[i-1,v-Ci]遞推而來的。換句話說,這正是為了保證每件物品之選一次,保證在考慮“選入第i件物品”這個策略時,依據的是一個絕無已經選入第i件物品的子結果F[i,v-Ci]。而現在完全背包的特點是每種物品可選無限件,所以在考慮“加選一件第i件物品”這種策略時,正需要一個可能已經選入第i種物品的子結果F[i,v-Ci],因此必須采用遞增的順序循環。
把上述式子寫成二維形式:F[i,v]=max{F[i-1,v],F[i,v-Ci]+wi}
總結
最后抽象出處理一件完全背包類物品的過程偽代碼:
CompletePack(F,C,W)
for v = C to V
F[v] = max{F[v],F[v-C]+W}
三、多重背包問題
題目
有N種物品和一個容量為V的背包。第i種物品最多有Mi件,放入第i件物品耗費的空間是Ci,得到的價值是Wi。求解將哪些物品裝入背包可使價值總和最大。
思路
這題和完全背包很相似。只是每種物品最多能取的個數不同了。
照貓畫虎,寫出狀態轉移方程:
F[i,v] = max{F[i-1,v-k*Ci]+k*Wi | 0<=k<=Mi}
復雜度是O(V*ΣMi)
轉化為01背包問題
仍然考慮二進制的思想,我們考慮把第i種物品換成若干件物品,使得原問題中第i種物品可取的每種策略——取0……Mi件——均能等價於取若干件代換以后的物品。另外,取超過Mi的策略不能出現。
方法是:將第i種物品分成若干件01背包中的物品,其中每一件物品有一個系數。這件物品的費用和價值均是原來的費用和價值乘以這個系數。令這些系數分別為1,2,2^2...2^(k-1),Mi-2^k+1,且k是滿足Mi-2^k+1>0的最大整數。例如Mi=13,那么k=3,對應系數分別為1,2,4,6。
分成的這幾件物品的系數和為Mi,表明不可能取多於Mi件的第i種物品。另外這種方法也能保證對於0...Mi間的每一個整數,均可以用若干個系數的和表示。這樣就把第i種物品分成了logMi種物品,將復雜度降到了O(V*ΣlogMi)。
總結
下面給出O(logM)時間處理一件多重背包中物品的過程:
MultiplePack(F,C,W,M)
if C*M >= V
CompletePack(F,C,W)
return;
k = 1
while k < M
ZeroOnePack(k*C,k*W)
M = M - k
k = 2*k
ZeroOnePack(M*C,M*W)
可行性問題O(V*N)的算法
當問題是“每種有若干件物品能否填滿給定容量的背包”,只需考慮填滿背包的可行性,不需要考慮每件物品的價值時,多重背包問題同樣有復雜度O(V*N)的算法。
下面介紹一種方法:設F[i,j]表示“用了前i種物品填滿容量為j的背包后,最多還剩下幾個第i種物品可用”,如果F[i,j]=-1則說明這種狀態不可行,若可行應,滿足0<=F[i,j]<=Mi。
遞推求F[i,j]的偽代碼如下:
F[0,1...V] = -1
F[0,0] = 0
for i = 1 to N
for j = 0 to V
if F[i-1,j] >= 0
F[i,j] = Mi
else
F[i,j] = -1
for j = 0 to V-Ci
if F[i,j] > 0
F[i,j+Ci] = max{F[i,j+Ci,F[i,j]-1}
最終F[N,0...V]便是多重背包可行性問題的答案。
四、混合三種背包問題
若有的物品只能取一次,有的物品可取無限次,有的物品可取Mi次。該怎么求解呢?
最清晰的做法時使用上述定義的三個過程:
for i = 1 to N
if 第i件物品屬於01背包
ZeroOnePack(F,Ci,Wi)
else if 第i件物品屬於完全背包
CompletePack(F,Ci,Wi)
else if 第i件物品屬於多重背包
MultiplePack(F,Ci,Wi,Mi)