1. 程式人生 > >【演算法詳解】揹包模板或模型詳解

【演算法詳解】揹包模板或模型詳解

01揹包

有N 件物品和一個容量為V 的揹包。放入第i 件物品耗費的空間 是vi,得到的價值是wi。求解將哪些物品裝入揹包可使價值總和最大。

根據題意,我們便可以設f[i][j]為已經裝了i件物品且最大容量為j得最大價值.
此時,我們就可以列舉i和j進行動態規劃的狀態轉移得到結論.
那麼f[i][j]的最大價值是怎麼轉移的呢?
當第i件物品不選的時候,相當於前i件物品相同的體積j的最大價值
若不選,最大價值則是前i件物品體積為j-這個物品體積的最大價值加上最個物品價值
因此,我們可以得到01揹包的狀態轉移方程:

f[i][j]=max(f[i1][j],f[i1][jvi]+wi)
但是,我們仍然可以在空間上選擇優化:
f[j]=max(f[j],f[jvi]+wi)
每一層i的狀態只和i-1有關,因此我們可以用1維的陣列表示.但是當前面的j轉移之後列舉到後面的j是顯然需要用先前j來列舉,無法滿足動態規劃的無後效性,因此我們再列舉j的時候需要倒敘列舉.
程式碼如下:
#include<bits/stdc++.h>
using namespace std;
int n,V;
int v[100000];
int w[100000];
int
f[1000000]; int main() { ios::sync_with_stdio; cin>>n>>V; for (int i=1;i<=n;i++) cin>>v[i]>>w[i]; for (int i=1;i<=n;i++) for (int j=V;j>=v[i];j--) f[j]=max(f[j],f[j-v[i]]+w[i]); cout<<f[V]; return 0; }

完全揹包

有N 件物品和一個容量為V 的揹包,每件物品都有無限件。放入 第i 件物品耗費的空間是vi,得到的價值是wi。求解將哪些物品裝入背 包可使價值總和最大

根據01揹包的求解思路,則有:

f[i][j]=max(f[i1][j],f[i][jvi]+wi)
就可以表示為不選和選過後可以再選,因此第二個方程是i而沒有-1
聯絡一下01揹包,01揹包為什麼要倒敘列舉,而不是正序列舉,是因為正序列舉是f[j-v[i]]這一步已經是被覆蓋的了,即有可能已經取過第i件物品,矛盾了01揹包的特性,因此要倒序;但是卻符合完全揹包,所以i將01揹包倒過來即可
程式碼如下:
#include<bits/stdc++.h>
using namespace std;
int n,V;
int v[100000];
int w[100000];
int f[1000000];
int main()
{
    ios::sync_with_stdio(false); 
    cin>>n>>V;
    for (int i=1;i<=n;i++)
        cin>>v[i]>>w[i];
    for (int i=1;i<=n;i++)
        for (int j=v[i];j<=V;j++)
            f[j]=max(f[j],f[j-v[i]]+w[i]);
    cout<<f[V];
    return 0;
} 

多重揹包

有N 件物品和一個容量為V 的揹包,第i 件物品都有ni 件。放入 第i 件物品耗費的空間是ci,得到的價值是wi。求解將哪些物品裝入背 包可使價值總和最大

顯然,我們可以選擇最暴力的做法,把每種情況列一遍,但是會TLE.因此需要二進位制優化:
以13為例:13用2的平方數之和為:1+2+4還差6,所以13=1+2+4+6
不難發現,這4個數字都可以組成1-13的任何一個數:
1,2,1+2,4,1+4,2+4,1+2+4,2+6,1+2+6,4+6,1+4+6,2+4+6,1+2+4+6
其實,這種方法並不只是適用於13這個數,任何數都是如此.
所以,我們就把一件物品按數量拆分成這些二進位制數和0或1個其它數字的和,然後再做01揹包,這樣就可以保證列舉到每一種的情況且時間複雜度要減少很多
程式碼如下:

#include<bits/stdc++.h>
using namespace std;
int n,V,tot=0;
int w[100000];
int v[100000];
int f[1000000];
int main()
{
    ios::sync_with_stdio; 
    cin>>n>>V;
    for (int i=1;i<=n;i++)
    {
        int stv,stw,num,temp=0;
        cin>>stv>>stw>>num;
        while (num-(1<<temp)>=0)
        {
            w[++tot]=stw*(1<<temp);
            v[tot]=stv*(1<<temp);
            num-=1<<temp;
            temp++;
        }
        if (num) w[++tot]=stw*num,v[tot]=stv*num;
    }
    for (int i=1;i<=tot;i++)
        for (int j=V;j>=v[i];j--)
            f[j]=max(f[j],f[j-v[i]]+w[i]);
    cout<<f[V];
    return 0;
} 

三種揹包(01,完全,多重揹包)混合

#include<bits/stdc++.h>
using namespace std;
int n,V;
int f[100000];
其餘變數...... 
void Zeroonebag(int v,int w)
{
    做01揹包 
}
void Allbag(int v,int w)
{
    做完全揹包 
}
void Manybag(int v,int w,int n)
{
    做多重揹包 
} 
int main()
{
    ios::sync_with_stdio(false); 
    int n,V;
    cin>>n>>V;
    for (int i=1;i<=n;i++)
    {
        int stv,stw;
        cin>>stv>>stw>>num
        if (num==1) Zeroonebag(stv,stw);
        if (num*stv>V) Allbag(stv,stw);
        if (num>1&&num*stv<=V) Manybag(stv,stw,num); 
    }
    cout<<f[V];
    return 0;
} 

二維費用揹包

有N 件物品,放入第i 件物品耗費的兩種空間代價是ci 和di,得到 的價值是wi。兩種代價可付出的最大值(兩種揹包容量)分別為V 和U。求解將哪些物品裝入揹包可使價值總和最大

事實上,這就是01揹包,只不過多了一種空間和狀態,我們只需要再原來的狀態上面多一維並在程式的實現上面多一種迴圈去列舉即可.
設f[i][j]表示第一種空間用了i,第二種空間用了j的最大價值,那麼我們很容易地得到:

f[i][j]=max(f[ici][jdi]+wi,f[i][j])
程式碼如下:
#include<bits/stdc++.h>
using namespace std;
int n;
int V,U;
int c[100000];
int d[100000];
int w[100000];
int f[5000][5000];
int main()
{
    ios::sync_with_stdio;
    cin>>n>>V>>U;
    for (int i=1;i<=n;i++)
        cin>>c[i]>>d[i]>>w[i];
    for (int i=1;i<=n;i++)
        for (int j=V;j>=c[i];j--)
            for (int k=U;k>=d[i];k--)
                f[j][k]=max(f[j][k],f[j-c[i]][k-d[i]]+w[i]);
    cout<<f[V][U];
    return 0;
}

分組揹包

有N 件物品和一個容量為V 的揹包。第i 件物品的空間代價是ci, 價值是wi。這些物品被劃分為K 組,每組中的物品互相沖突,最多選一 件。求解將哪些物品裝入揹包可使價值總和最大。

這種揹包的問題同樣十分簡單,只要再狀態的設定上略加改動即可.
設f [i][j] 表示前i 組物品恰放入容量j的揹包可以獲得的最大價值。
我們便可以得到狀態轉移方程:

f[i][j]=max(f[i1][j],f[i1][jci]+wi)(ik)
對於其狀態轉移的過程,可以參考下面老師PPT的程式碼:
for(int i=1;i<=K;i++) 
    for(int j=V;j>=0;j−−) 
        for( item i in groupKl) 
            f [j] = max(f [j],f [j −ci] + wi);

我們可以思考思考一下:難道不會出現每件物品都重複的問題嗎?
事實上是不會的,根據迴圈順序,每件物品都有一個固定的j且j為倒敘列舉,即向前列舉的時候沒有任意一個同組的物品存在前面狀態中,故不會重複.
但是我們需要注意:這個迴圈的順序是不能打亂的,必然就會產生重複的現象.
程式碼如下:

#include<bits/stdc++.h>
using namespace std;
int K,V;
int tot[10000];
int f[1000000];
int v[2000][2000];
int w[2000][2000];
int main()
{
    ios::sync_with_stdio;
    cin>>K>>V;
    for (int i=1;i<=K;i++)
    {
        cin>>tot[i];
        for (int j=1;j<=tot[i];j++)
            cin>>v[i][j]>>w[i][j];
    }
    for (int i=1;i<=K;i++)
        for (int j=V;j>=0;j--)
            for (int k=1;k<=tot[i];k++)
                if (j-v[i][k]>=0)
                    f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);
    cout<<f[V];
    return 0;
}