1. 程式人生 > >【揹包問題】 01揹包+完全揹包+多重揹包

【揹包問題】 01揹包+完全揹包+多重揹包

前言

揹包問題(Knapsack problem)是一種組合優化的NP完全問題。問題可以描述為:給定一組物品,每種物品都有自己的重量和價格,在限定的總重量內,我們如何選擇,才能使得物品的總價格最高。問題的名稱來源於如何選擇最合適的物品放置於給定揹包中。相似問題經常出現在商業、組合數學,計算複雜性理論、密碼學和應用數學等領域中。也可以將揹包問題描述為決定性問題,即在總重量不超過W的前提下,總價值是否能達到V?它是在1978年由Merkel和Hellman提出的。

一、01揹包

問題描述:有N件物品和一個容量為V的揹包。第i件物品的重量是w[i],價值是v[i]。求解將哪些物品裝入揹包可使這些物品的重量總和不超過揹包容量,且價值總和最大。

基本思路:這是最基礎的揹包問題,特點是:每種物品僅有一件,可以選擇放或不放。用子問題定義狀態:即f[i][v]表示前i件物品恰放入一個容量為v的揹包可以獲得的最大價值。則其狀態轉移方程便是:f[i][v]=max( f[i-1][v], f[i-1][v-w[i]]+v[i] )。可以壓縮空間:f[v]=max( f[v],f[v-w[i]]+v[i] )這個方程非常重要,基本上所有跟揹包相關的問題的方程都是由它衍生出來的。所以有必要將它詳細解釋一下:"將前i件物品放入容量為v的揹包中"這個子問題,若只考慮第i件物品的策略(放或不放),那麼就可以轉化為一個只牽扯前i-1件物品的問題。

1.

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

2. 如果放第i件物品,那麼問題就轉化為"前i-1件物品放入剩下的容量為v-w[i]的揹包中",此時能獲得的最大價值就是f [i-1][v-w[i]]再加上通過放入第i件物品獲得的價值v[i]。

注意:對 f[ ][ ]初始化時,若題目要求揹包恰好裝滿,那麼f[0][0]初始化為0,其他初始化為-INF。若題目沒有要求揹包恰好裝滿,那麼對f[ ][ ]陣列全部初始化為0。

//01揹包核心程式碼
//初始化f[][]陣列
f[0][0]=0;
for(int i=1;i<=n;i++)//要求揹包恰好裝滿
    for(int j=0;j<=V;j++)
        f[i][j]=-INF;
for(int i=1;i<=n;i++)//不要求揹包是否裝滿
    for(int j=0;j<=V;j++)
        f[i][j]=0;
for(int i=1;i<=n;i++)
{
    for(int j=0;j<=V;j++)
    {
        f[i][j]=f[i-1][j];
        if(j>=w[i])
            f[i][j]=max(f[i][j],f[i-1][j-w[i]]+v[i]);
    }
}

優化:以上方法的時間和空間複雜度均為O(N*V),其中時間複雜度基本已經不能再優化了,但空間複雜度卻可以優化到O(V)。

先考慮上面講的基本思路如何實現,肯定是有一個主迴圈i=1..N,每次算出來二維陣列f[i][0..V]的所有值。那麼,如果只用一個數組f [0..V],能不能保證第i次迴圈結束後f[v]中表示的就是我們定義的狀態f[i][v]呢?

f[i][v]是由f[i-1][v]和f [i-1][v-w[i]]兩個子問題遞推而來,能否保證在推f[v]時(也即在第i次主迴圈中推f[v]時)能夠得到f[v]和f[v -w[i]]的值呢?事實上,這要求在每次主迴圈中我們以v=V..0的順序推f[v],這樣才能保證推f[v]時f[v-w[i]]儲存的是狀態f[i-1][v-c[i]]的值。虛擬碼如下:

for i=1..N

for v=V..0

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

其中的f[v]=max{f[v],f[v-w[i]]}一句恰就相當於我們的轉移方程f[i][v]=max{f[i-1][v],f[i-1][v-w[i]]},因為的

f[v-w[i]]就相當於原來的f[i-1][v-w[i]]。如果將v的迴圈順序從上面的逆序改成順序的話,那麼則成了f[i][v]由f[i][v-w[i]]推知,與本題意不符,但它卻是另一個重要的揹包問題完全揹包最簡捷的解決方案,故學習只用一維陣列解01揹包問題是十分必要的。

//優化後的程式碼,降低了空間複雜度
for(int i=1;i<=n;i++)
{
    for(int j=V;j>=w[i];j--)//注意一定要逆序迴圈
    {
        f[j]=max(f[j],f[j-w[i]]+v[i]);
    }
}

二、完全揹包

問題描述:有N種物品和一個容量為V的揹包,每種物品都有無限件可用。第i種物品的體積是c,價值是w。將哪些物品裝入揹包可使這些物品的體積總和不超過揹包容量,且價值總和最大。

基本思路:這個問題非常類似於01揹包問題,所不同的是每種物品有無限件,也就是從每種物品的角度考慮,與它相關的策略已並非取或不取兩種,而是有取0件、取1件、取2件……取[V/c]件等很多種。如果仍然按照解01揹包時的思路,令f[v]表示前i種物品恰放入一個容量為v的揹包的最大權值。仍然可以按照每種物品不同的策略寫出狀態轉移方程,像這樣:f[i][j]=max{f[i-1][j],f[i-1][j-k*c]+k*w}(0<=k*c<=v)這跟01揹包問題一樣有O(N*V)個狀態需要求解,但求解每個狀態f[v]的時間是O(V/c),總的複雜度是超過O(VN)的。

注意:對 f[ ][ ]初始化時與01揹包相同,若題目要求揹包恰好裝滿,那麼f[0][0]初始化為0,其他初始化為-INF。若題目沒有要求揹包恰好裝滿,那麼對f[ ][ ]陣列全部初始化為0。

//完全揹包核心程式碼
for(int i=1;i<=n;i++)
{
    for(int j=V;j>=c[i];j--)
    {
        for(int k=0;k*c[i]<=j;k++)
            f[i][j]=max(f[i-1][j],f[i-1][j-k*c[i]]+k*w[i]);
    }
}

優化:完全揹包問題有一個很簡單有效的優化,是這樣的:若兩件物品i、j滿足c[i]<=c[j]且w[i]>=w[j],則將物品j去掉,不用考慮。這個優化的正確性顯然:任何情況下都可將價值小費用高得j換成物美價廉的i,得到至少不會更差的方案。首先將費用大於V的物品去掉,然後使用類似計數排序的做法,計算出費用相同的物品中價值最高的是哪個,可以O(V+N)地完成這個優化。

既然01揹包問題是最基本的揹包問題,那麼我們可以考慮把完全揹包問題轉化為01揹包問題來解。最簡單的想法是,考慮到第i種物品最多選V/c件,於是可以把第i種物品轉化為V/c件費用為c[I]及價值w[I]的物品,然後求解這個01揹包問題。這樣完全沒有改進基本思路的時間複雜度,但這畢竟給了我們將完全揹包問題轉化為01揹包問題的思路:將一種物品拆成多件物品。

//完全揹包優化後代碼
for(int i=1;i<=n;i++)
{
    for(int j=c[i];j<=V;j--)//順序迴圈
    {
        f[j]=max(f[j],f[j-c[i]]+w[i]);
    }
}

迴圈順序及原因:為什麼01揹包中要按照v=V...0逆序來迴圈。這是因為要保證第i次迴圈中的狀態f[v]是由狀態f[v-c]遞推而來。換句話說,這正是為了保證每件物品只選一次,保證在考慮“選入第i件物品”這件策略時,依據的是一個沒有已經選入第i件物品的子結果f[v-c]。而當前完全揹包的特點恰是每種物品可選無限件,所以在考慮“加選一件第i種物品”這種策略時,卻正需要一個可能已選入第i種物品的子結果f[v-c],所以就可以並且必須採用v=0..V的順序迴圈。

三、多重揹包

問題描述:有N種物品和一個容量為V的揹包。第i種物品最多有n件可用,每件體積是c,價值是w。求解將哪些物品裝入揹包可使這些物品的體積總和不超過揹包容量,且價值總和最大。

基本思路:這題目和完全揹包問題很類似。基本的方程只需將完全揹包問題的方程略微一改即可,因為對於第i種物品有n+1種策略:取0件,取1件……取 n件。令f[v]表示前i種物品恰放入一個容量為v的揹包的最大權值,則:f[v]=max{f[v-k*c]+ k*w|0<=k<=n}。複雜度是O(V*∑n)。另一種好想好寫的基該方法是轉化為01揹包求解:把第i種物品換成n件01揹包中的物品,則得到了物品數為∑n的01揹包問題,直接求解,複雜度仍然是O(V*∑n)。

//多重揹包程式碼
for(int i=1;i<=m;i++)
{
    cin>>v[i]>>w[i]>>c[i];
    for(int j=0;j<c[i];j++)//列舉第i種物品的數目
    {
        for(int k=n;k>=v[i];k--)
            dp[k]=max(dp[k],dp[k-v[i]]+w[i]);
    }
}

總結

01揹包問題是最基本的揹包問題,它包含了揹包問題中設計狀態、方程的最基本思想,另外,別的型別的揹包問題往往也可以轉換成01揹包問題求解。故一定要仔細體會上面基本思路的得出方法,狀態轉移方程的意義,以及最後怎樣優化的空間複雜度。完全揹包問題和多重揹包問題都是基礎的揹包問題,有兩個狀態轉移方程,都可以由01揹包擴充套件而來。

推薦文章:dd大牛的《揹包九講》

推薦題目:HDU-2546(01揹包)HDU-1114(完全揹包)HDU-2191(多重揹包)

AC程式碼:

//HDU-2546(01揹包)
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <string>
#include <cmath>
#include <queue>
#include <stack>
#include <vector>
#include <map>
#include <set>
using namespace std;
#define io ios::sync_with_stdio(0),cin.tie(0)
#define ms(arr) memset(arr,0,sizeof(arr))
#define inf 0x3f3f3f
typedef long long ll;
const int mod=1e9+7;
const int maxn=1e3+7;
int n,v;
int w[maxn];
int dp[maxn];
int main()
{
    io;
    while(cin>>n&&n)
    {
        memset(dp,0,sizeof(dp));
        for(int i=1;i<=n;i++)
            cin>>w[i];
        cin>>v;
        sort(w+1,w+1+n);
        if(v<5)
            cout<<v<<endl;
        else
        {
            for(int i=1;i<n;i++)
                for(int j=v-5;j>=w[i];j--)
                    dp[j]=max(dp[j],dp[j-w[i]]+w[i]);
            cout<<v-dp[v-5]-w[n]<<endl;
        }
    }
    return 0;
}
//HDU-1114(完全揹包)
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <string>
#include <cmath>
#include <queue>
#include <stack>
#include <vector>
#include <map>
#include <set>
using namespace std;
#define io ios::sync_with_stdio(0),cin.tie(0)
#define ms(arr) memset(arr,0,sizeof(arr))
#define inf 0x3f3f3f
typedef long long ll;
const int mod=1e9+7;
const int maxn=1e5+7;
int t;
int e,f,n,p,w;
int dp[maxn];
int main()
{
    io;
    cin>>t;
    while(t--)
    {
        cin>>e>>f;
        dp[0]=0;
        for(int i=1;i<=f;i++)
            dp[i]=inf;
        cin>>n;
        for(int i=1;i<=n;i++)
        {
            cin>>p>>w;
            for(int j=w;j<=f-e;j++)
                dp[j]=min(dp[j],dp[j-w]+p);
        }
        if(dp[f-e]==inf)
            cout<<"This is impossible."<<endl;
        else
            cout<<"The minimum amount of money in the piggy-bank is "<<dp[f-e]<<"."<<endl;
    }
    return 0;
}
//HDU-2191(多重揹包)
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <string>
#include <cmath>
#include <queue>
#include <stack>
#include <vector>
#include <map>
#include <set>
using namespace std;
#define io ios::sync_with_stdio(0),cin.tie(0)
#define ms(arr) memset(arr,0,sizeof(arr))
#define inf 0x3f3f3f
typedef long long ll;
const int mod=1e9+7;
const int maxn=110;
int t,n,m;
int v[110],w[110],c[110];
int dp[110];
int main()
{
    cin>>t;
    while(t--)
    {
        cin>>n>>m;
        ms(dp);
        for(int i=1;i<=m;i++)
        {
            cin>>v[i]>>w[i]>>c[i];
            for(int j=0;j<c[i];j++)
            {
                for(int k=n;k>=v[i];k--)
                    dp[k]=max(dp[k],dp[k-v[i]]+w[i]);
            }
        }
        cout<<dp[n]<<endl;
    }
    return 0;
}