1. 程式人生 > >揹包問題 動態規劃 滾動陣列實現

揹包問題 動態規劃 滾動陣列實現

比起別的講解揹包問題的部落格,這一篇更加註重在於理解如何用滾動陣列實現動態規劃。

揹包問題特徵
1.存在類似一個集合的求解所有子集的特性。關於一個集合的所有子集,會直接考慮每一個集合元素存在或不存在於子集中,最後對於一個由n個元素構成的集合,其子集數目是2的n次方個。也就是1/0問題。
2.在某些約束下,取捨以達到某種目的,一般為優化,也有一些是求解組合。

基礎的揹包問題

有N件物品和一個容量為V的揹包。第i件物品的重量是w[i],價值是v[i]。求解將哪些物品裝入揹包可使這些物品的重量總和不超過揹包容量,且價值總和最大。每種物品僅有一件,可以選擇放或不放。百度百科

每一種物品可以選擇放進揹包或者不放進揹包,那就一件一件物品來考量。
定義 dp[i][j] 表示遍歷到第 i 個物品的時候,放入物品 i 或者不放入物品 i ,揹包容量為 j 時的最大價值。兩種情況:

(1)放進物品 i : 那其價值就等於 dp[i-1][j - w[i]] + value[i]。第一項是指揹包裡有物品 i 時,改變了原來用前 i - 1個物品填充揹包的內容,其原來揹包的價值已經發生改變,變成了用前面遍歷過的 i - 1個物品來填充容量 j - w[i] 的揹包的最大價值。第二項是本身物品 i 帶來的價值。
(2)不放進物品 i :那就是僅僅使用前 i - 1 個物品來填充容量為 j 的揹包的最大價值。

要計算dp[i][j] 則是在上面兩種情況裡面選擇能夠得到最大價值的情況。

轉移方程為 dp[i][j] = max{ dp[i-1][j - w[i]] + value[i], dp[i-1][j] }

.

二維陣列實現

int volumn, numItem;
int weights[numItem + 1], value[numItem + 1];
//TODO:初始化揹包總容量volumn, 物體種數numItem,物體重量陣列weights,物體價值陣列value
int dp[numItem + 1][volumn + 1];
//TODO: 初始化dp全為0
for(int i = 1; i<= numItem; i++){
    //對於揹包容量 j 小於物體容量weights[i]的情況不需要考慮
    for(int j = 1; j <= volumn; j++){
        if
(j < weights[i]){ dp[i][j] = dp[i-1][j]; }else{ dp[i][j] = max(dp[i-1][j], dp[i-1][j-weights[i]] + value[i]); } } } return dp[numItem][volumn];//揹包最大價值

使用滾動陣列實現

重新觀察轉移方程 dp[i][j] = max{ dp[i-1][j - w[i]] + value[i], dp[i-1][j] }
發現 dp[i] 的狀態僅僅和 dp[i-1]有關,所以僅僅只需要儲存 i-1 時刻dp的狀態。
考慮用一個一位陣列 dp, dp[j] 表示揹包容量 j 時的最大價值。
在考慮是否要放進物體 i 的之前,此時 dp[j] 陣列儲存的狀態還是用前 i - 1 個物體放進容量為 j 時候的最大價值。所以可以直接用原來的dp[j] 來代替原來的 dp[i-1][j]。
對於狀態方程的一項 dp[i-1][j - w[i]] + value[i],可以明確 j - w[i] < j。因為考慮物體 i 時需要更新的 dp[j] (即dp[i][j])需要通過dp[i-1][j - w[i]]來計算。為了保證使用的dp[j - w[i]]是僅考慮完第 i - 1個物體時候的值,所以dp[j - w[i]]的值更新要發生在dp[j]之後。又因為 j - w[i] < j,所以 dp[j] 需要逆序更新。如果是順序更新,那麼容量為 j - w[i] 和容量為 j 兩種情況下,很有可能會放進同一個物體兩次只要該物體的容量比 j - w[i]還小,所以不符合這個揹包問題的約束。

int volumn, numItem;
int weights[numItem + 1], value[numItem + 1];
//TODO:初始化揹包總容量volumn, 物體種數numItem,物體重量陣列weights,物體價值陣列value
int dp[volumn + 1];
//TODO: 初始化dp全為0
for(int i = 1; i<= numItem; i++){
    //對於揹包容量 j 小於物體容量weights[i]的情況不需要考慮
    for(int j = volumn; j >= weights[i]; j++){
        dp[j] = max(dp[j], dp[j-weights[i]] + value[i]);
        }
    }
}
return dp[numItem][volumn];//揹包最大價值

在之前的二維陣列實現裡面,有一個if的判斷,

if(j < weights[i]){
    dp[i][j] = dp[i-1][j];
}else{
    dp[i][j] = max(dp[i-1][j], dp[i-1][j-weights[i]] + value[i]);
}

滾動陣列實現中,由於在考慮物體 i 時,dp[j]不更新則保留了考慮物體 i - 1 時候的狀態,所以這裡的判斷可以轉化成滾動陣列實現裡面的 j 從 volumn遞減到weight[i]為止即可。

完全揹包問題

有N件物品和一個容量為V的揹包。第i件物品的重量是w[i],價值是v[i]。求解將哪些物品裝入揹包可使這些物品的重量總和不超過揹包容量,且價值總和最大。每種物品有無窮多件,可以選擇放或不放。

轉移方程為 f[i][j]=max{ f[i-1][j-k*w[i]]+k*value[i]|0<=k*w[i]<=j }.

順序列舉揹包容量時,由於 j - w[i] < j , 則會先計算 j - w[i]。在揹包容量為 j - w[i] 時,一旦裝入了物品 i,由於求f[v]需要使用f[i - 1][ j - w[i]],而若求f[v]時也可以裝入物品 i 的話,那麼在揹包容量為 j 時就裝入兩次物品 i 。又若 j - w[i]是由之前的狀態推出,它們也成功裝入物品 i 的話,那麼容量為 j 的揹包就裝入了多次物品 i 了。
注意,此時,在計算 f[j] 時,已經把物品 i 能裝入的全裝入容量為 j 的揹包了,此時裝入物品 i 的次數為最大。

順序列舉容量是完全揹包問題最簡捷的解決方案。

int volumn, numItem;
int weights[numItem + 1], value[numItem + 1];
//TODO:初始化揹包總容量volumn, 物體種數numItem,物體重量陣列weights,物體價值陣列value
int dp[volumn + 1];
//TODO: 初始化dp全為0
for(int i = 1; i<= numItem; i++){
    //對於揹包容量 j 小於物體容量weights[i]的情況不需要考慮
    for(int j = weights[i]; j <= volumn; j++){
        dp[j] = max(dp[j], dp[j-weights[i]] + value[i]);
        }
    }
}
return dp[numItem][volumn];//揹包最大價值

多重揹包問題

有N種物品和一個容量為V的揹包。第i種物品最多有n[i]件可用,每件重量是w[i],價值是value[i]。求解將哪些物品裝入揹包可使這些物品的重量總和不超過揹包容量,且價值總和最大。

轉移方程 f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i]|0<=k<=n[i]}

一個直接的方法是加多一重迴圈去迴圈k在遍歷物品和遍歷揹包容量中間。複雜度是O(V*Σn[i])。

另外一個是將n[i]件物品 分解成多個不同重量的新物體,重量倍數分別為一件物品的1,2,4,…,2^(k-1),n[i]-2^k+1(且k是滿足n[i]-2^k+1>0的最大整數)倍,然後再求解。複雜度會降到O(V*Σlog n[i])。二進位制優化了解

程式碼如下

int k, t;
k = 1;
t = n[i];
while(t > k)
{
    for(j=W; j>=c[i]*k; --j)
    {
        f[j] = max(f[j], f[j-c[i]*k] + w[i]*k);
    }
    t -= k;
    k *= 2;
}
for(j=W; j>=c[i]*t; --j)
{
    f[j] = max(f[j], f[j-c[i]*t] + w[i]*t);
}

到這裡,幾個比較基礎的揹包問題已經講完了。動態規劃的轉移方程如果能寫出來,基本上就能解決問題了。不過,這就是最難的地方了。