背包问题是一类比较大的问题,问法也比较多,所以(dd大牛)大佬写了背包九讲,在这里我想先总结一下前三讲,一方面巩固自己,另一方面希望能给大家带来一定的帮助,愿与诸君共勉;
题目源自https://www.acwing.com/problem/可以对照这题目来看,如果新接触的话
01背包问题
给你n给物品,每个物品有体积和价值w,一个m体积的背包
在n个物品中选择,使得既不超容积,又价值最大
1 #include <iostream> 2 #include<bitset> 3 using namespace std; 4 const int N=1010; 5 int n,m; 6 int f[N][N];//f[i][j]表示选到i物品,在使用体积为恰好j的情况下的价值最大值 7 int main() 8 { 9 cin>>n>>m; 10 for(int i=1;i<=n;i++) 11 { 12 int v,w; 13 cin>>v>>w; 14 for(int j=0;j<=m;j++) 15 { 16 if(j-v>=0) 17 f[i][j]=max(f[i-1][j],f[i-1][j-v]+w);//如果有可能装下当前物品,则进行一下比较 18 else 19 f[i][j]=f[i-1][j]; //如果装不下当前物品,上一个状态也不能丢 20 } 21 } 22 int ma=0; 23 for(int i=0;i<=m;i++)//因为是使用体积恰好为j,所以还要进行一次遍历 24 ma=max(f[n][i],ma); 25 cout<<ma; 26 return 0; 27 }
01背包还可以进行空间上的优化
#include <iostream> #include<bitset> using namespace std; const int N=1010; int n,m; int f[N];//f[i]表示体积最大为i的时候的价值 int main() { cin>>n>>m; for(int i=1;i<=n;i++) { int v,w; cin>>v>>w; for(int j=m;j>=v;j--)//因为每个物品只能用一次,所以反着枚举 { f[j]=max(f[j-v]+w,f[j]);//j-v不可能小于0,所以不存在判断了 } } cout<<f[m]; return 0; }
01背包到此结束
完全背包问题
背景同01背包,但是每个物品有无限多个
#include <iostream> #include<bitset> using namespace std; const int N=1010; int n,m; int f[N];//f[i]表示体积最大为i的时候的价值 int main() { cin>>n>>m; for(int i=1;i<=n;i++) { int v,w; cin>>v>>w; for(int j=0;j<=m;j++) { if(j-v>=0) f[j]=max(f[j-v]+w,f[j]); } } cout<<f[m]; return 0; }
可以发现只是将01背包的1维形式枚举体积时的顺序反过来了,同样的也可以用2维实现,具体的就不贴代码了
接下来是重头戏
多重背包问题
背景同01背包,但是n代表物品种类数,每次给出物品种类时,会给出s代表该类物品的个数
那么想法很快就来了,把他拆成s个01背包不久得了么?
确实下面给出关键代码,不然都没人看了!qaq
for(int i=1;i<=n;i++) { int v,w,s; cin>>v>>w>>s; for(int j=m;j>=v;j--)//枚举体积 { for(int k=0;k<=s;k++)//枚举决策 { if(j-k*v>=0) f[j]=max(f[j],f[j-k*v]+k*w); } } }
但是这样的时间复杂度有点过于高了
n*m*s,似乎只能解决100的问题,1000就1e9了,1s绝对TLE
于是就有了下面的方法(请继续往下看)
多重背包之二进制拆分
假设给你一个数,要你用尽可能少的数,能够组合成小于该数的所有的数
假设10
1 1 1 1 1 1 1 1 1 1 是可以的,也就是我们上面的方法
1 2 4 3 同样是可行的,明显下面一种更优
1 #include <iostream> 2 #include<bitset> 3 #include<vector> 4 using namespace std; 5 const int N=2010; 6 int n,m; 7 struct node 8 { 9 int v,w; 10 }; 11 vector<node> ve; 12 int f[N]; 13 int main() 14 { 15 cin>>n>>m; 16 for(int i=1;i<=n;i++) 17 { 18 int v,w,s; 19 cin>>v>>w>>s; 20 int t=1; 21 while(s>=t) 22 { 23 s-=t; 24 ve.push_back({t*v,t*w}); 25 t*=2; 26 } 27 if(s) 28 ve.push_back({s*v,s*w}); 29 } 30 //接下来就是01背包了 31 for(auto x:ve)//枚举物品 32 { 33 for(int j=m;j>=x.v;j--)//枚举体积 34 { 35 f[j]=max(f[j],f[j-x.v]+x.w); 36 } 37 } 38 cout<<f[m]; 39 return 0; 40 }
时间复杂度为n*m*log(s),对付1000级别的问题没点问题,但是要是上10000了,就搞不定了
1e4*1e4*log(1e4)=3e9,所以我们得把log(s)得去掉,所以就有了下面的多重背包终极版
多重背包之单调队列优化
如果不会单调队列的可以去做一下滑动窗口这道题,就算你会单调队列,以我的水平我也不保证能让大佬们听懂qaq(请关爱萌新的我)
首先,单调队列是求区间最值的最快的方法,但是使用他的条件比较的苛刻,具体说来就是必须区间等长,而且询问从左至右
仔细观察多重背包转移的方程,可以发现对于某一个物品f[i]只能从f[i-v]转移过来,所以我们按余数将状态分组,所以就变成了求与f[i]%v相同的体积的最大值
但是此时滑动窗口的大小还没有确定,窗口大小k=min(m/v,s),意思就是当体积非常大的时候,你只能拿s个,所以此时窗口大小为s,而背包体积较小的时候,你只能装下m/v个,所以此时窗口大小为m/v
(其实当v*s大于m时,直接从头至尾枚举每个和j对v取余相同的体积,求出最大值,也是可行的,但是此时为了代码的统一,这样处理起来更加方便),如果看到这里不是很懂,可以看下代码的注释,语文差;
1 #include <iostream> 2 #include<bitset> 3 #include<vector> 4 using namespace std; 5 const int N=20100; 6 int n,m; 7 int f[N]; 8 struct node 9 { 10 int id; 11 int val; 12 }; 13 node qu[N]; 14 int main() 15 { 16 cin>>n>>m; 17 for(int i=1;i<=n;i++)////枚举物品 18 { 19 int v,w,s; 20 cin>>v>>w>>s; 21 int k=min(s,m/v);//窗口大小 22 for(int mod=0;mod<v;mod++)//枚举余数 23 { 24 int tail=-1; 25 int head=0; 26 for(int l=0;l<=(m-mod)/v;l++)//枚举决策,从头到尾(%v余数相同) 27 { 28 int x=l,y=f[l*v+mod]-l*w;//x为单调队列的编号,同时也是装多少个 29 //注意理解y,当枚举l+1时,因为空间都增长了一个v,那么价值至少会增加一个w 30 //而此时我们做的是选择出同组最大的价值,所以要保证在同一个起跑线上,所以要减去一个w 31 //这样说吧,就是你决策为l+1时,可能不是选的这个物品,价值大于w,但是你减去w之后,它和其他的组内成员在同一起跑线,而他更大,所以选他 32 if(tail<head) 33 { 34 qu[++tail]={x,y}; 35 } 36 else 37 { 38 while(tail>=head&&qu[head].id<l-k)head++; 39 while(tail>=head&&qu[tail].val<=y)tail--; 40 qu[++tail]={x,y}; 41 } 42 f[l*v+mod]=qu[head].val+l*w;//f[j]等于额外的价值加上选当前物品的价值 43 } 44 } 45 } 46 cout<<f[m]; 47 return 0; 48 }
在你懂单调队列的情况下,如果你能看懂那一大串注释的话,多重背包终极版你就掌握了,如果看不懂的话,请在心里暗道一句傻逼之后,去其他大佬的博客逛一逛,祝你弄懂这个。