揹包九講(部分)
根據這幾天的學習情況,總結一下對於揹包的理解和一些實現方式:
1.大名鼎鼎的0/1揹包:這個就不多總結了
2.完全揹包:
應該明白,通俗意義上完全揹包指的是對於n個價值為v,重量為w的物品,每個物品可以無限次的取(而對於0/1來講,則是隻能取一次)
這怎麼處理?
如果按照解01揹包時的思路,令 f[i][j]表示前i種物品恰放入一個容量為v的揹包的最大權值。仍然可以按照每種物品不同的策略寫出狀態轉移方程,像這樣:
f[i][j]=max(f[i][j],f[i-1][j-k*w[i]+k*v[i]);
但很不幸的是,這種情況大多是超時的
我們用滾動陣列優化,具體優化方式如下:
既然01揹包問題是最基本的揹包問題,那麼我們可以考慮把完全揹包問題轉化為01揹包問題來解。最簡單的想法是,考慮到第 i i i種物品最多選 V/c[i]件,於是可以把第 i種物品轉化為 V/c[i]件費用及價值均不變的物品,然後求解這個01揹包問題。這樣完全沒有改進基本思路的時間複雜度,但這畢竟給了我們將完全揹包問題轉化為01揹包問題的思路:將一種物品拆成多件物品。
1 for (int i = 1; i <= n; i++)//大迴圈 2 for (int j = c[i]; j <= V; j++)//正序啦,注意是正序 3 dp[j] = max([dpj], dp[j - c[i]] + w[i]);//狀態以及轉移方程
首先想想為什麼01揹包中要按照 j = V . . . 0 逆序來迴圈。這是因為要保證第 i次迴圈中的狀態f[i][j]是由狀態f[i−1][j−c[i]]遞推而來。換句話說,這正是為了保證每件物品只選一次,保證在考慮“選入第i件物品”這件策略時,依據的是一個絕無已經選入第i件物品的子結果 f[i−1][j−c[i]]。而現在完全揹包的特點恰是每種物品可選無限件,所以在考慮“加選一件第i種物品”這種策略時,卻正需要一個可能已選入第 i種物品的子結果f[i][j−c[i]],所以就可以並且必須採用 j = 0... V 的順序迴圈。
實戰:https://www.acwing.com/problem/content/3/
https://www.luogu.com.cn/problem/P1853#sub
以上兩個題的參考程式碼以及注意事項如下:
#include<bits/stdc++.h> using namespace std; int n,v; int va[1010]; int w[1010]; int dp[1010]; int main() { ios::sync_with_stdio(false); cin>>n>>v; for(register int i=1;i<=n;i++) cin>>w[i]>>va[i];for(register int i=1;i<=n;i++) { for(register int j=w[i];j<=v;j++) { dp[j]=max(dp[j],dp[j-w[i]]+va[i]); } } cout<<dp[v]<<endl; return 0; }
洛谷:
1 #include<bits/stdc++.h> 2 using namespace std; 3 struct node 4 { 5 int weight; 6 int value; 7 }a[51]; 8 int s; 9 int n; 10 int d; 11 int dp[10000010]; 12 int main(){ 13 cin>>s>>n>>d; 14 for(int i=1;i<=d;i++){ 15 cin>>a[i].weight>>a[i].value; 16 } 17 for(int k=1;k<=n;k++){//最外層是迴圈重新整理s值的,否則s一直為本金 18 for(int i=1;i<=d;i++){//完全揹包從這裡開始(i,j) 19 for(int j=a[i].weight;j<=s;j++){ 20 dp[j]=max(dp[j],dp[j-a[i].weight]+a[i].value);//完全揹包公式 21 } 22 } 23 s=s+dp[s];//重新整理s的值 24 } 25 cout<<s; 26 return 0; 27 }
3.多重揹包:
多重揹包無非是設了限定條件,每個物品最多可以取s[i]件,程式碼如下,不在多解釋
for (int i = 1; i <= N; i++) for (int j = V; j >= w[i]; j--) for (int k = 1; k <= p[i] && k * w[i] <= j; k++) f[j] = max(f[j], f[j - w[i] * k] + v[i] * k);
這裡重點要說的是多重揹包的二進位制優化以及單調佇列優化(單調佇列優化用的不太熟,就不多說了)
我們來講講二進位制優化:將第i種物品分成若干件物品,其中每件物品有一個係數,這件物品的費用和價值均是原來的費用和價值乘以這個係數。使這些係數分別為1,2,4,...2^k-1,s[i]-2^k+1;
並且s[i]-2^k+1>0的最大整數。如對於20來講,可分成1,2,4,8,5,我們要取出7個數,我們只要把4,1,2推出來就好啦,這裡面有一個分蘋果問題,不在多述。
另外這種方法也能保證對於 0... s[i] 間的每一個整數,均可以用若干個係數的和表示,這個證明可以分 0... 2^k-1, 2^k---s[i]兩端分別得出,在時間上是遠遠優化於原來大迴圈演算法的,具有log運算的優越性
1 for(register int i=1;i<=n;i++) 2 { 3 int num=min(s[i],v/weight[i]);//最多放進取的數目 4 for(register int k=1;num>0;k<<=1)//二進位制操作符,不會的話取百度,不多講 5 { 6 if(k>num) 7 k=num; 8 num-=k; 9 for(register int j=v;j>=k*weight[i];j--)//轉化為0/1 10 { 11 dp[j]=max(dp[j],dp[j-k*weight[i]]+k*value[i]); 12 } 13 } 14 }
實戰專案:https://www.luogu.com.cn/problem/P1776
#include<bits/stdc++.h> using namespace std; int n,w; int value[1000010]; int weight[100010]; int dp[1000010]; int s[1000010];//上限 int main() { ios::sync_with_stdio(false); cin>>n>>w; for(register int i=1;i<=n;i++) cin>>value[i]>>weight[i]>>s[i]; for(register int i=1;i<=n;i++) { int num=min(s[i],w/weight[i]);//最多可以放多少 for(register int k=1;num>0;k<<=1) { if(k>num) k=num;//如果二進位制係數超過了最大值,那就讓最大值等於二進位制係數 num-=k; //減去消耗的 for(register int j=w;j>=k*weight[i];j--)//縱浪大化中,不喜亦不懼,化0/1揹包,如是而已 { dp[j]=max(dp[j],dp[j-k*weight[i]]+k*value[i]); } } } cout<<dp[w]<<endl; return 0; }
4.二維揹包問題:
二維費用的揹包問題是指:對於每件物品,具有兩種不同的費用;選擇這件物品必須同時付出這兩種代價;對於每種代價都有一個可付出的最大值(揹包容量)。問怎樣選擇物品可以得到最大的價值。設第i件物品所需的兩種代價分別為c[i]和g[i]。兩種代價可付出的最大值(兩種揹包容量)分別為V和 M。物品的價值為w[i]。
思路:無非多了一個費用,那就開三維陣列;
dp[i][j][k]=max(dp[i][j][k],dp[i-1][j-weight[i][k-weight2[i]+value[i]);
1 for (int i = 1; i <= n; i++) 2 for (int j = V; j >= c[i]; j--) 3 for (int k = M; k >= g[i]; k--) 4 f[j][k] = max(f[j][k], f[j - c[i]][k - g[i]] + w[i]);
在源頭上也是屬於0/1;
實戰:https://www.luogu.com.cn/problem/P1507;
https://www.acwing.com/problem/content/8/;
1 #pragma GCC optimize(2)//很普通的O2優化 2 #include<bits/stdc++.h> 3 using namespace std; 4 int n,v,m; 5 int tiji[1010]; 6 int value[1010]; 7 int weight[1010]; 8 int dp[1010][1010];//開三維陣列用二維的滾動陣列優化 9 int main() 10 { 11 ios::sync_with_stdio(false); 12 cin>>n>>v>>m; 13 for(register int i=1;i<=n;i++) 14 cin>>tiji[i]>>weight[i]>>value[i]; 15 for(register int i=1;i<=n;i++)//化0/1揹包問題 16 { 17 for(register int j=v;j>=tiji[i];j--)//依舊是倒敘,原因不多解釋 18 { 19 for(register int k=m;k>=weight[i];k--) 20 { 21 dp[j][k]=max(dp[j][k],dp[j-tiji[i]][k-weight[i]]+value[i]);//狀態以及轉移方程 22 } 23 } 24 } 25 cout<<dp[v][m]<<endl;//最後的輸出 26 return 0; 27 }
洛谷:
1 #include<bits/stdc++.h> 2 using namespace std; 3 int vmax; 4 int wmax; 5 int n; 6 int dp[1010][1010]; 7 int weight[1010]; 8 int tiji[1010]; 9 int value[1010]; 10 int main() 11 { 12 ios::sync_with_stdio(false); 13 cin>>vmax>>wmax>>n; 14 for(register int i=1;i<=n;i++) 15 cin>>tiji[i]>>weight[i]>>value[i]; 16 for(register int i=1;i<=n;i++) 17 { 18 for(register int j=vmax;j>=tiji[i];j--) 19 { 20 for(register int k=wmax;k>=weight[i];k--) 21 { 22 dp[j][k]=max(dp[j][k],dp[j-tiji[i]][k-weight[i]]+value[i]); 23 } 24 } 25 } 26 cout<<dp[vmax][wmax]<<endl; 27 return 0; 28 }
程式碼思路很明確,不多講了。
明天看情況更新泛化揹包以及混合揹包以及本講中沒有提及到的其他九講中的揹包。
加油加油,奔向遠方,成為百科全書式的人。
戒驕戒躁,任重道遠