20191310李燁龍第七八章讀書筆記
揹包問題總結
_ _ _ _ _ _ __ _ _ _ _ _ _ 複賽前的掙扎(望csp-s複賽 rp++)
這裡整理了6種揹包問題(雖說還有好幾種,但是本人太菜了~~~~)
- 01揹包
題目
有N 件物品和一個容量為V 的揹包。放入第i 件物品花費的費用是w[ i ] ,得到的價值是v[ i ] ,求將哪些物品裝入揹包可使價值總和最大。
基本思路
這是最基礎的揹包問題,特點是:每種物品僅有一件,可以選擇放或不放。 用子問題定義狀態:即f [ i ] [ j ] 表示前 i 件物品恰放入一個容量為 j 的揹包可以獲得的最大價值,則其狀態轉移方程便是:
f[i][j]=max(f[i-1][j],f[i-1][j-w[i]]+v[i])
解釋一下: “將前 i 件物品放入容量為 j 的揹包中”這個子問題,若只考慮第 i 件物品的策略(放或不放),那麼就可以轉化為一個只牽扯前 i − 1 件物品的問題。如果不放第 i 件物品,那麼問題就轉化為“前 i − 1 件物品放入容量為 j 的揹包中”,最大價值為 f [ i − 1 ] [ j ] ;如果放第 i 件物品,那麼問題就轉化為“前 i − 1 件物品放入剩下的容量為j − w[ i ] 的揹包中”,此時能獲得的最大價值就是f [ i − 1 ] [ j − w[ i ] ] 再加上通過放入第 i 件物品獲得的價值 w [ i ] 。
空間複雜度優化
以上方法的時間和空間複雜度均為Θ ( V ∗ N ) 其中時間複雜度已經不能再優化了,但空間複雜度卻可以優化到Θ ( N )。 程式碼如下:
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]);
你以為這就完了?注意看下面:
一個常數優化
前面的程式碼中有f o r ( j = V . . . w [ i ] ) ,還可以將這個迴圈的下限進行改進。 假設當前物品為i ,即使在後面的迴圈中i + 1... n 的所有物體都要取,該下限也能保證最優解f [ V ] 能被更新到,該方法就是避免了這種情況,相當於捨棄了一些無用的狀態,減少迴圈次數,但相應也降低了陣列的完整性。f [ j − w [ i ] ]中的j jj減去後面所有的w [ i ] 才能達到這個bound,所以bound以下的狀態是對答案沒有貢獻的。程式碼可以改成:
for(int i=1;i<=n;i++){
int bound=max(V-sum{w[i+1]...w[n],w[i]);
}
for(int j=V;j>=bound;j--){
f[j]=max(f[j],f[j-w[i]]+v[i]);
}
對於求sum可以用字首和,這對於V 比較大時是有用的。 具體程式碼:
for(int i=1;i<=n;i++)
cin>>w[i]>>v[i],sum[i]=sum[i-1]+w[i];
for(int i=1;i<=n;i++){
int bound=max(w[i],V-(sum[n]-sum[i]));
for(int j=V;j>=bound;j--){
f[j]=max(f[j],f[j-w[i]]+v[i]);
}
}
- 完全揹包
題目
有N 種物品和一個容量為V 的揹包,每種物品都有無限件可用。第i 種物品的費用是w[ i ] ,價值是v[ i ] 。求解將哪些物品裝入揹包可使這些物品的費用總和不超過揹包容量,且價值總和最大。
基本思路
這個問題非常類似於01揹包問題,所不同的是每種物品有無限件。也就是從每種物品的角度考慮,與它相關的策略已並非取或不取兩種,而是有取0 件、取1 件、取2 件……等很多種。如果仍然按照解01揹包時的思路,令f [ i ] [ j ] 表示前i 種物品恰放入一個容量為V 的揹包的最大權值。仍然可以按照每種物品不同的策略寫出狀態轉移方程:
f[i][j]=max(f[i-1][j-k*w[i]]+k*v[i]))∣0≤k∗w[i]<=j
但是其複雜度為Θ ( N ∗ Σ ( V / w[ i ] ) ) 是比較大的,所以我們可以將其轉化成01揹包求解,變為Θ(V∗N):
f[i][j]=max(f[i−1][j],f[i][j−c[i]]+w[i])
同理,將其滾掉一維後變為:
for(int i=1;i<=n;i++)
for(int j=w[i];j<=V;j++)
f[j]=max(f[j],f[j-w[i]]+v[i]);
細心的讀者會發現,這個程式碼與01揹包的程式碼只有 j 的迴圈次序不同而已。為什麼這樣一改就可行呢?首先想想為什麼01揹包中要按照 j = V . . . 0 的逆序來迴圈。這是因為要保證第i 次迴圈中的狀態 f [ i ] [ j ] 是由狀態 f [ i − 1 ] [ j − w[ i ] ] 遞推而來。換句話說,這正是為了保證每件物品只選一次,保證在考慮“選入第 i 件物品”這件策略時,依據的是一個絕無已經選入第 i 件物品的子結果 f [ i − 1 ] [ j − w[ i ] ] 。而現在完全揹包的特點恰是每種物品可選無限件,所以在考慮“加選一件第i 種物品”這種策略時,卻正需要一個可能已選入第i 種物品的子結果 f [ i ] [ j − w[ i ] ] ,所以就可以並且必須採用 j = 0... V 的順序迴圈。
若還未明白正序倒序問題可參考此篇部落格~~~~
- 多重揹包
題目
有 N 種物品和一個容量為 V的揹包。第 i 種物品最多有 p [ i ] 件可用,每件費用是 c [ i ] ,價值是 w [ i ] 。求解將哪些物品裝入揹包可使這些物品的費用總和不超過揹包容量,且價值總和最大。
基本演算法
這題目和完全揹包問題很類似。基本的方程只需將完全揹包問題的方程略微一改即可,因為對於第 i 種物品有 p [ i ] + 1 種策略:取0 件,取1 件……取 p [ i ] 件。令 f [ i ] [ j ] 表示前 i 種物品恰放入一個容量為 j 的揹包的最大價值,則有狀態轉移方程:
f[i][j]=max(f[i-1][j-k*w[i]]+k*v[i])∣0≤k≤p[i]
實際程式碼實現中再加一重 0... p [ i ] 的迴圈即可,複雜度是Θ ( V ∗ ∑ p [ i ] ) ),如下:
for(int i=1;i<=n;i++)
for(int j=V;j>=w[i];j--)
for(int k=1;k<=p[i];k++)
f[j]=max(f[j],f[j-w[i]*k]+v[i]*k);
當然,這也可以轉化為時間複雜度為Θ ( N ∗ l o g ( p ) ∗ V ) )的01揹包。二分制拆分程式碼如下:
/*方法一*/
int p[N];
int prime3(int n,int V){
for(int i=1;i<=n;i++){
int num=min(p[i],V/w[i]);
for(int k=1;num>0;k<<=1){
if(k>num) k=num;
num-=k;
for(int j=V;j>=w[i]*k;j++){
f[j]=max(f[j],f[j-w[i]*k]+v[i]*k);
}
}
}
return f[V];
}
/*方法二*/
int numv[N],numw[N],cnt;
int prime4(int n,int V){
for(int i=1;i<=n;i++){
for(int j=1;j<=p[i];j<<=1){
numv[cnt]=j*v[i];
numw[++cnt]=j*w[i];
p[i]-=j;
}
if(p[i]>0){
numv[cnt]=p[i]*v[i];
numv[++cnt]=p[i]*w[i];
}
}
for(int i=1;i<=cnt;i++)
for(int j=V;j>=numw[i];j--)
f[j]=max(f[j],f[j-numw[i]]+numv[i]);
return f[V];
}
- 混合揹包
問題
如果將前面三個揹包混合起來,也就是說,有的物品只可以取一次(01揹包),有的物品可以取無限次(完全揹包),有的物品可以取的次數有一個上限(多重揹包),應該怎麼求解呢?
程式碼如下:
for(int i=1;i<=n;i++){
cin>>w[i]>>v[i]>>p[i];
if(p[i]==0)//完全揹包
for(int j=w[i];j<=V;j++)
f[j]=max(f[j],f[j-w[i]]+v[i]);
else if(p[i]==1)//01揹包
for(int j=V;j>=w[i];j--)
f[j]=max(f[j],f[j-w[i]]+v[i]);
else {//二分制拆分優化多重揹包
int num=min(p[i],V/w[i]);
for(int k=1;num>0;k<<=1){
if(num>=k) num=k;
num-=k;
for(int j=V;j>=w[i]*k;j--)
f[j]=max(f[j],f[j-w[i]*k]+v[i]*k);
}
}
}
- 二維費用揹包
問題
二維費用的揹包問題是指:對於每件物品,具有兩種不同的費用;選擇這件物品必須同時付出這兩種代價;對於每種代價都有一個可付出的最大值(揹包容量)。問怎樣選擇物品可以得到最大的價值。設第 i 品所需的兩種代價分別為 w[ i ] 和 g[ i ] 。兩種代價可付出的最大值(兩種揹包容量)分別為 V和 M。物品的價值為 v[ i ] 。
演算法
費用加了一維,只需狀態也加一維即可。設f [ i ] [ j ] [ k ] 表示前i 件物品付出兩種代價分別最大為j 和k 獲得的最大價值。狀態轉移方程就是:
f[i][j][k]=max(f[i−1][j][k],f[i−1][j−w[i]][k-g[i]]+v[i])
滾掉一維後:
for(int i=1;i<=n;i++)
for(int j=T;j>=w[i];j--)
for(int k=V;k>=g[i];k--)
dp[j][k]=max(dp[j][k],dp[j-w[i]][k-g[i]]+v[i]);
這裡要注意一點:有時,“二維費用”的條件是以這樣一種隱含的方式給出的:最多隻能取T件物品。這事實上相當於每件物品多了一種“件數”的費用,每個物品的件數費用均為1 ,可以付出的最大件數費用為T。換句話說,設f [ i ] [ j ]表示付出費用i 、最多選j 件時可得到的最大價值,則根據物品的型別(01、完全、多重)用不同的方法迴圈更新。
- 分組揹包
問題
有N 件物品和一個容量為V 的揹包。第i ii件物品的費用是w[ i ] ,價值是v[ i ] 。這些物品被劃分為若干組,每組中的物品互相沖突,最多選一件。求解將哪些物品裝入揹包可使這些物品的費用總和不超過揹包容量,且價值總和最大。
演算法
這個問題變成了每組物品有若干種策略:是選擇本組的某一件,還是一件都不選。也就是說設f [ k ] [ j ] 表示前k kk組物品花費費用j 能取得的最大價值,則有:
f[k][j]=max(f[k-1][j],f[k-1][j-w[i]]+v[i]∣物品i⊆組k)
同樣可用一維陣列:
/*方法一*/
int a[1010][1010];
int prime7(){
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
cin>>a[i][j];
for(int i=1;i<=n;i++)
for(int j=m;j>=0;j--)
for(int k=1;k<=j;k++)
f[j]=max(f[j],f[j-k]+a[i][k]);
return f[m];
}
/*方法二*/
int s;
int c[N];
inline void prime8(){
for(int i=1;i<=n;i++){
cin>>s;//物品組數
}
for(int j=1;j<=s;j++) cin>>c[j]>>w[j];
for(int j=V;j>0;j--)
for(int k=1;k<=s;k++)
if(j>=c[k])
f[j]=max(f[j],f[j-c[k]]+c[k]);
}
好了,到這裡就結束了。。。
最後附上揹包程式碼全家桶:
#include<bits/stdc++.h>
#define N 1000010
using namespace std;
int f[N],w[N],v[N];
/*---------0-1揹包---------*/
inline int prime1(int n,int V){
memset(f,0x3f3f3f,sizeof(f)); f[0]=0;//需要裝滿
memset(f,0,sizeof(f));//不需要裝滿
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]);
return f[V];
}
/*---------完全揹包---------*/
int prime2(int n,int V){
for(int i=1;i<=n;i++)
for(int j=w[i];j<=V;j++)
f[j]=max(f[j],f[j-w[i]]+v[i]);
return f[V];
}
/*----------多重揹包二進位制拆分--------*/
/*方法一*/
int p[N];
int prime3(int n,int V){
for(int i=1;i<=n;i++){
int num=min(p[i],V/w[i]);
for(int k=1;num>0;k<<=1){
if(k>num) k=num;
num-=k;
for(int j=V;j>=w[i]*k;j++){
f[j]=max(f[j],f[j-w[i]*k]+v[i]*k);
}
}
}
return f[V];
}
/*方法二*/
int numv[N],numw[N],cnt;
int prime4(int n,int V){
for(int i=1;i<=n;i++){
for(int j=1;j<=p[i];j<<=1){
numv[cnt]=j*v[i];
numw[++cnt]=j*w[i];
p[i]-=j;
}
if(p[i]>0){
numv[cnt]=p[i]*v[i];
numv[++cnt]=p[i]*w[i];
}
}
for(int i=1;i<=cnt;i++)
for(int j=V;j>=numw[i];j--)
f[j]=max(f[j],f[j-numw[i]]+numv[i]);
return f[V];
}
/*------------二維費用揹包----------*/
int t[N],g[N],dp[1010][1010];
int n,m;
int prime6(int n,int V,int T){
for(int i=1;i<=n;i++)
for(int j=T;j>=w[i];j--)
for(int k=V;k>=g[i];k--)
dp[j][k]=max(dp[j][k],dp[j-w[i]][k-g[i]]+v[i]);
return dp[T][V];
}
/*------------分組揹包-----------*/
/*方法一*/
int a[1010][1010];
int prime7(){
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
cin>>a[i][j];
for(int i=1;i<=n;i++)
for(int j=m;j>=0;j--)
for(int k=1;k<=j;k++)
f[j]=max(f[j],f[j-k]+a[i][k]);
return f[m];
}
/*方法二*/
int s;
int c[N];
inline void prime8(){
for(int i=1;i<=n;i++){
cin>>s;//物品組數
}
for(int j=1;j<=s;j++) cin>>c[j]>>w[j];
for(int j=c[j];j>0;j--)
for(int k=1;k<=s;k++)
if(j>=c[k])
f[j]=max(f[j],f[j-c[k]]+c[k]);
}
完結撒花~~~~