[揹包九講1]
P1.01基礎揹包問題
對於N個寶石,每個寶石的價值為vi,重量花費為wi。揹包的總載重量為W,則試問對於一個揹包這麼放寶石才能使其裝的寶石總價值最大。
具體思路:考慮狀態,利用i表示第i個寶石,j表示當前揹包的已用空間,d[i][j]就可以表示當前狀況下揹包內寶石的最大價值。則要求的問題可以轉化為d[N][W]的求取。
然後構建狀態轉移方程:
d[i][j]=max{d[i−1][j],d[i−1][j−w[i]]+v[i]}d[i][j]=max{d[i−1][j],d[i−1][j−w[i]]+v[i]}
值得注意的是這裡max內只有兩項是因為一個寶石只有選和不選兩種情況,所以DP狀態轉移方程的思想就是:
當前狀態 = 取最優{前一狀態1,前一狀態2…前一狀態n}
從上可以看出狀態的選取很重要,並且由於是通過將最終問題不斷分為前一種狀態下的最優解(即最優子結構),所以要採用自底向上的方式計算,即先計算最小問題單元,並記錄,然後計算後一級問題時就可以以直接呼叫節約時間。所以時自底向上。也就是對解空間樹從計算葉子問題開始逐步推向主問題。
仔細考慮這一過程也可以發現,在逐步上推的過程中,母問題的決策方案是不會對之前的子問題造成影響的,因此這就是無後效性的表現。(葉子問題的決策->子問題1的決策->…->子問題n的決策->母問題的決策)
虛擬碼為:
d[0][0...W] = 0;
for i = 1->N
for j = 0->W
if(w[i] > j)
d[i][j] =d[i-1][j]
else
d[i][j] = max{d[i-1][j],d[i-1][j-w[i]]+v[i]}
圖中青色代表d[2][5],可以看出他由綠色的兩個各種中的數值得出。即當前狀態由前一狀態的兩個解中的最優解得出。接下來黃色的三個圈也一樣。因此DP的路線就是從左上角不斷計算得到右下角的最終問題的解。要注意的是DP有個賦予初值的步驟。
問題優化
剛好放滿的求解方法
由上圖看出,如果我想求解剛好放滿揹包時候的揹包的最大價值,可以採用的方法就是初值的賦予技巧,即只對d[0][0] = 0,d[0][1…W]都賦予負無窮,這樣就可以篩選掉不滿的情況。
空間的優化
從這張圖裡面還可以看出,每個寶石i的選擇,其實只需要上一次的i-1時候的選擇情況。因此其實可以用一個一維陣列而不是二維陣列表示。
先看虛擬碼
d[0...W] = 0;
for i = 1->N
for j = W->0 //這裡是唯一的不同
if(w[i] > j)
d[j] =d[j]
else
d[j] = max{d[j],d[j-w[i]]+v[i]}
虛擬碼只是對第二個for迴圈的順序做了改變。即改為了從W到0,然後就把陣列換成了一維陣列。
結合上圖我們可以看下發生了什麼。那個一維陣列記憶體取的東西如圖紅色部分。是兩個狀態的結合。可以看出,當我要求d[2][6]時,我需要的d[1][6]和d[1][1],轉換到一維陣列時,就是d[6]和d[1],然後算出來新的d[6]對原陣列進行覆蓋。這樣就完成了空間的壓縮
進一步優化
在壓縮到一維陣列後,其實我們還可以繼續優化,即對二層迴圈的上下限做手腳。
虛擬碼如下:
d[0...W] = 0;
for i = 1->N
for j = W->w[i] //下限不同了
/*
if(w[i] > j)
d[j] =d[j]
else
*/
d[j] = max{d[j],d[j-w[i]]+v[i]}
為何是w[i]呢?考慮下之前的情況和圖,如果j
再進一步
上一節是從左邊考慮遍歷的節省界限。即去掉從0開始到w[i]的部分。
這一節是從右邊考慮遍歷的節省界限。
考慮對於結果第n個寶石,其考慮的就是d[W]項,那其前一項,其實只需要提供從W到W-w[n]項即可。如下圖藍色項。因此,對於第i個寶石時,我們指控考慮的d[j]項的範圍為:W到max{w[i],W−sum(w[i],w[i+1],...,w[n])}W到max{w[i],W−sum(w[i],w[i+1],...,w[n])}
d[0...W] = 0; for i = 1->N for j = W->max{w[i],W-sum(w[i->n])} d[j] = max{d[j],d[j-w[i]]+v[i]}
public void zeroOnePack(w[i], v[i]){
for j = W->max{w[i],W-sum(w[i->n])}
d[j] = max{d[j],d[j-w[i]]+v[i]}
}
P2.完全揹包問題
假若每種寶石的數量不設上限,則問題轉變為完全揹包問題。
該問題的求解有兩種方法,一個是基礎方法,一個抽象方法。
基礎方法
其實說是無限,還是有限的,即一種寶石數量肯定小於揹包容量V/w[i]
考慮子狀態,最直接的就是
d[i][j]=max{d[i−1][j−k∗w[i]]+k∗v[i]|0≤k≤W/w[i]}d[i][j]=max{d[i−1][j−k∗w[i]]+k∗v[i]|0≤k≤W/w[i]}
即當前狀態 = 取最優{前一狀態1,前一狀態2…前一狀態n}
改進方法是,將一種寶石ni個看成ni個統一規格的寶石,則總共有sum(n1, n2, … , nn)個寶石,大迴圈為
for i = 1 -> sum(n1, n2, … , nn)
這個方法只是換了個角度看問題,並沒有降低代價。不過從這個角度考慮,其實我們可以考慮對同一種寶石,我們拿的個數可以等價為一顆大寶石,比如,取2顆寶石i則相當於取了一顆中2*w[i],價值2*v[i]的大寶石。
現在的情況就是如何構建各種大寶石來保證取的時候不會重複,這時候可以考慮計算機的二進位制儲存方式(只要有2的0,1,…,n次方的數,就可以組合出任意1到2的n次方之間的數)。構建的大寶石價值應該為1顆i,2顆i,4顆i。。。2的k次方顆i。這樣,我只要對這些構造的大寶石進行是否裝入判斷,就可以判斷出最優解是裝幾顆寶石i,其中k要滿足w[i]∗2k≤Ww[i]∗2k≤W。
虛擬碼
for i = 1->N
k = 1
while(k <= W/w[i])
zeroOnePack(k*w[i],k*v[i])
k = k*2
其實上式是有冗餘的,即,k不用取那麼大,只要保證1,2,4,…,k的寶石加起來總的個數是小於W/w[i]的最大值就可以,而不用非要第k個是小於W/w[i]的最大值。因此構建新的k的邊界應該是sum(1,2,4,…,k)<=W/w[i]時的最大值。並且為了保證總和為W/w[i],還要再補上一顆(W/w[i]-sum(1,2,4,…,k))*w[i]的大寶石。
偽碼如下:
for i = 1->N
k = 1
amount = W/w[i]
while(k <= amount)
zeroOnePack(k*w[i],k*v[i])
amount = amount - k
k = k*2
zeroOnePack(k*w[i],k*v[i])
抽象方法—更快
還是這張圖,可以看出,當初為了保證是從d[i-1][j],d[i-1][j-w[i]]推出d[i][j],我們採用從W到w[i]的方式,其目的就是保證不會產生從d[i][0]推出[i][j]的情況,現在,既然是一個寶石i要有無限個,那麼明顯可以採用從d[i][0]推出[i][j]的情況,所以給出的for迴圈是從0->W.
虛擬碼如下:
d[0...W] = 0;
for i = 1->N
for j = 0->W
if(w[i] > j)
d[j] =d[j]
else
d[j] = max{d[j],d[j-w[i]]+v[i]}
我們可以定義該過程為一個新方法:
public void completePack(w[i], v[i]){
for j = 0->W
if(w[i] > j)
d[j] =d[j]
else
d[j] = max{d[j],d[j-w[i]]+v[i]}
}
P3.多重揹包
多重揹包在這裡指的是當寶石i的個數給定為ni時的揹包問題。這裡可以考慮,當ni = 1時,就是01揹包問題,當ni大於W/w[i]時,就是完全揹包問題。當ni在兩者之間時,可以轉換成完全揹包問題裡面的基礎解法。因此可以構造如下虛擬碼:
public void multiplePack(w[i], v[i], amount)
if(amount > W/w[i])
completePack(w[i], v[i])
else{
k = 1
amount = W/w[i]
while(k <= amount)
zeroOnePack(k*w[i],k*v[i])
amount = amount - k
k = k*2
zeroOnePack(k*w[i],k*v[i])
}
P9.揹包問法變化
構造最優解
構造最優解的方法,我學到的有兩種。
第一種是採用01基礎揹包方案時,得到了d[i][j],若令i = N;j = W;則可以根據求出的二維陣列進行逆推求出每個最優選擇。虛擬碼如下:
public getOptSolution(){
i = N
j = W
while(d[i][j] > d[0][j])
if d[i][j] == d[i-1][j]
沒有選擇第i個寶石
i = i - 1
else
選擇了第i個寶石
i = i - 1
j = j - w[i]
}
第二中是採用了一維陣列去進行計算的時候,特別是在求取多重揹包或者完全揹包問題時的構造最優解方法,關鍵就是建立輔助的陣列進行幫忙記錄,不過由於在多重揹包問題時,每種寶石的具體展開大寶石數量事先計算比較麻煩,因此可以採用動態陣列ArrayList來幫忙。
虛擬碼:
//--------------------構造記錄用動態陣列(在大迴圈中)------------------
d[0->W] = 0
動態陣列recorder
for i = 1->N
multiplePack(w[i], v[i], amount)
//--------------------改進01揹包方法,增加記錄陣列(在小迴圈中)----------------
public void zeroOnePack(w[i], v[i]){
boolean[] rec
for j = W->w[i]
rec[j] = isPut(d[j],d[j-w[i]]+v[i]) //構造子方法判斷,判斷依據同max
d[j] = max{d[j],d[j-w[i]]+v[i]}
recorder.add(rec)
//---------------------構造最優解-------------------------
同 getOptSolution()方法
補充:
其實只要理解多重揹包的基礎方法,是可以不用構造記錄陣列也可以完成最優解的構造的。不過會比較麻煩。在此再提及一下多重揹包的思想:
假設原來有N種寶石1,2,..,N.每種有n[i]個
則現在多重揹包的基礎方法的思想是轉換成M種寶石,分別為
(n[1],2*n[1],4*n[1],…,k[1]*n[1]),(n[2],2*n[2],4*n[2],…,k[2]*n[2]),…………….,(n[N],2*n[N],4*n[N],…,k[N]*n[N])即可。