完全揹包問題(動態規劃)

完全揹包問題

題目:

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

 

 

一、驗證動態規劃求解的可行性

 

這個問題可以不可以像01揹包問題一樣使用動態規劃來求解呢?來證明一下即可。

首先,先用反證法證明最優化原理:

假設完全揹包的解爲F(n1,n2,...,nN)(n1,n2 分別代表第1、第2件物品的選取數量),完全揹包的子問題爲,將前i種物品放入容量爲t的揹包並取得最大價值,其對應的解爲:F(n1,n2,...,ni),假設該解不是子問題的最優解,即存在另一組解F(m1,m2,...,mi),使得F(m1,m2,...,mi) > F(n1,n2,...,ni),那麼F(m1,m2,...,mi,...,nN) 必然大於 F(n1,n2,...,nN),因此 F(n1,n2,...,nN) 不是原問題的最優解,與原假設不符,所以F(n1,n2,...,ni)必然是子問題的最優解。

再來看看無後效性:

對於子問題的任意解,都不會影響後續子問題的解,也就是說,前i種物品如何選擇,只要最終的剩餘揹包空間不變,就不會影響後面物品的選擇。即滿足無後效性。

因此,完全揹包問題也可以使用動態規劃來解決。

 

二、基本思路

 

這個問題非常類似於 01揹包問題,所不同的是每種物品都有無限件。也就是從每種物品的角度考慮,與它相關的策略已並非取或不取兩種,而是有取 0 件、取 1 件、取 2 件 ......等很多種。如果仍然按照解 01揹包時的思路,令 f[i][v] 表示前 i 種物品恰放入一個容量爲 v 的揹包的最大權值。仍然可以按照每種物品不同的策略寫出狀態轉移方程:

狀態轉移方程:

這跟 01揹包問題一樣有  O(VN)  個狀態需要求解,但求解每個狀態的時間已經不是常數了,求解狀態  f[i][v]  的時間是  ,總的複雜度可以認爲是  ,是比較大的。

 

基本代碼如下:


#include <iostream>
#include <algorithm>
#define N 1002
using namespace std;

int f[N][N];
int w[N];
int v[N];

int main() {
    int n,W; cin >> n >> W;
    for(int i=1;i<=n;i++) {
        cin >> w[i] >> v[i];
    }
    for ( int i = 1; i <= n; i++ ) {
        for ( int j = 0; j <= W; j ++) {
            for (int k = 0; k*w <= j; k++) {
                f[i][j] = max(f[i-1][j],f[i-1][j-w[i]*k] + v[i]*k);
            }
        }
    }
    cout << f[n][W] <<endl;
    return 0;
}

 

狀態轉移方程:同 01 揹包問題一樣,完全揹包問題空間複雜度可以優化到Θ(N)1。

空間複雜度優化後代碼如下:


#include <iostream>
#include <algorithm>
#define N 1002
using namespace std;

int f[N];
int w[N];
int v[N];

int main() {
    int n,W; cin >> n >> W;
    for(int i=1;i<=n;i++) {
        cin >> w[i] >> v[i];
    }
    for ( int i = 1; i <= n; i++ ) {
        for ( int j = W; j >= 0; j --) {
            for (int k = 0; k*w <= j; k++) {
                f[j] = max(f[j],f[j-w[i]*k] + v[i]*k);
            }
        }
    }
    cout << f[W] <<endl;
    return 0;
}
 
 

二、一個簡單有效的優化

完全揹包問題有一個很簡單有效的優化,是這樣的:若兩件物品 i 、j 滿足  且   ,則將物品 j 去掉,不用考慮。這個優化的正確性顯然:任何情況下都可以將價值小費用高的 j 換成物美價廉的 i ,得到至少不會更差的方案。對於隨機生成的數據,這個方法往往會大大減少物品的件數,從而加快速度。這個優化可以簡單的 Θ(N2) 實現,一般都可以承受。

然而這個並不能改善最壞情況的複雜度,因爲有可能特別設計的數據可以一件物品也去不掉。另外,針對揹包問題而言,比較不錯的一種方法是:首先將費用大於 V 的物品去掉,然後使用類似計數排序的做法,計算出費用相同的物品中價值最高的哪個,可以 Θ(V + N) 地完成這個優化。

 

三、轉化爲01揹包問題求解 

既然 01揹包問題是最基本的揹包問題,那麼我們可以考慮把完全揹包問題轉化爲 01揹包問題來解。最簡單的想法是,考慮到第 i 種物品最多選  V/c[i]  件,於是可以把第 i 種物品轉化爲  V/c[i]  件費用及價值均不變的物品,然後求解這個 01揹包問題。

這樣完全沒有改進基本思路的複雜度,但畢竟給了我們將完全揹包問題轉化爲 01揹包問題的思路:將一種物品拆成多件物品。

更高效的轉化方法是:把第 i 種物品拆成費用爲  、價值爲   的若干件物品,其中 k 滿足    。這是二進制的思想,因爲不管最優策略選幾件第 i 種物品,總可以表示成若干個  件物品的和。這樣把每種物品拆成    件物品,是一個很大的改進。

 
 

四、O(VN)的算法

這個算法使用一維數組,先看僞代碼:

for i=1..N
    for v=0..V
        f[v]=max{f[v],f[v-cost]+weight}
 

你會發現,這個僞代碼與 01揹包問題的僞代碼只有v的循環次序不同而已。

爲什麼這樣一改就可行呢?首先想想爲什麼01揹包問題中要按照v=V..0的逆序來循環。

這是因爲要保證第i次循環中的狀態f[i][v]是由狀態f[i-1][v-c[i]]遞推而來。換句話說,這正是爲了保證每件物品只選一次,保證在考慮“選入第i件物品”這件策略時,依據的是一個絕無已經選入第i件物品的子結果f[i-1][v-c[i]]。

而現在完全揹包的特點恰是每種物品可選無限件,所以在考慮“加選一件第i種物品”這種策略時,卻正需要一個可能已選入第i種物品的子結果f[i][v-c[i]],所以就可以並且必須採用v=0..V的順序循環。這就是這個簡單的程序爲何成立的道理。

 

值得一提的是,上面的僞代碼中兩層for循環的次序可以顛倒。這個結論有可能會帶來算法時間常數上的優化。

這個算法也可以以另外的思路得出。例如,將基本思路中求解f[i][v-c[i]]的狀態轉移方程顯式地寫出來,代入原方程中,會發現該方程可以等價地變形成這種形式:

f[i][v]=max{f[i-1][v],f[i][v-c[i]]+w[i]}

這個方程表示的意思:

“ 將前 i 件物品放入容量爲 v 的揹包中 ” 這個子問題,若只考慮第 i 件物品的策略(放多少個),那麼就可以轉化爲一個只牽扯前 i 件物品的問題。

如果不放第 i 件物品,那麼問題就轉化爲 “ 前 i-1 件物品放入容器爲 v 的揹包中 ” ,價值爲 f[i-1][v] ;

如果放第 i 件物品,那麼問題就轉化爲 “ 前 i 件物品(這裏不是前 i-1 的原因是,這裏的物品都有無限件,可能此前已經放了第 i 件物品)放入剩下的容量爲 v-c[i] 的揹包中 ”,揹包中此時能獲得的最大價值就是 f[i][v-c[i]] 價值再加上通過放入第 i 件物品獲得的價值 w[i]。

代碼:


#include <iostream>
#include <algorithm>
#define N 1002
using namespace std;

int f[N];
int w[N];
int v[N];

int main() {
    int n,W; cin >> n >> W;
    for(int i=1;i<=n;i++) {
        cin >> w[i] >> v[i];
    }
    for ( int i = 1; i <= n; i++ ) {
        for ( int j = w[i]; j <= W; j ++) {
            f[j] = max(f[j],f[j-w[i]] + v[i]);
        }
    }
    cout << f[W] <<endl;
    return 0;
}

 

最後抽象出處理一件完全揹包類物品的過程僞代碼:

procedure CompletePack(cost,weight)
    for v=cost..V
        f[v]=max{f[v],f[v-c[i]]+w[i]}

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章