1. 程式人生 > >[背包九講1]

[背包九講1]

nbsp 目的 價值 mda arr owa 這一 [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])即可。

[背包九講1]