背包问题
OI复建,自娱产物
1.01背包
dp的要素:最优子结构、边界、状态转移方程。
01背包的二维状态转移方程:f [ i ] [ j ] = max ( f [ i - 1 ] [ j ] , f [ i - 1 ] [ j - c [ i ] ] + w [ i ] )
v可以从c[ i ] 开始正向循环(i-1保证了不会重复)
再解释一下状态转移方程:
对于第 n 件物品,实现最优解时,只有选与不选第 n 件物品两种方案。
如果不选,f [ n ] [ v ] = f [ n - 1 ] [ v ]
如果选,f [ n ] [ v ] = f [ n - 1 ] [ v - cn ] + wn,要让这个方案最优,显然需要 f [ n - 1 ] [ v - cn ] 最优
空间优化:
f [ j ] = max ( f [ j ] + f [ j - c [ i ] ] + w [ i ] )
为了避免一件物品被计入多次,空间从后向前更新
要求背包必须装满的初始化:将 f 初始化为 - ∞ ,f [ 0 ] = 0
整个背包装满,要求每个子背包必须装满
#include<bits/stdc++.h>//01 #define N 30001 using namespace std; int m,n,w[N],c[N],f[N]; int main(){ scanf("%d%d",&m,&n); for(int i=1;i<=n;i++) scanf("%d%d",&c[i],&w[i]); for(int i=1;i<=n;i++) for(int j=m;j>0;j--) if(j>=c[i]) f[j]=max(f[j],f[j-c[i]]+w[i]); printf("%d",f[m]); }
2.完全背包
循环空间时改为正向循环即可。
洛谷似乎没有模板题?
洛谷P1616:
#include<bits/stdc++.h> #define N 10001 #define M 10000001 using namespace std; long long n,t,c[N],w[N]; long long f[M]; int main(){ memset(f,0,sizeof(f)); scanf("%ld%ld",&t,&n); for(int i=1;i<=n;i++) scanf("%ld%ld",&c[i],&w[i]); for(int i=1;i<=n;i++) for(int j=c[i];j<=t;j++) f[j]=max(f[j],f[j-c[i]]+w[i]); printf("%ld",f[t]); }
优化:
显然,如果 c [ i ] > c [ j ] 且 w [ i ] < w [ j ],j 物品永远优于 i 物品。( 注意只在物品可无限次选取时适用,也就是说01和多重背包不能这样优化)
可以有效降低随机数据的时间复杂度
(背包九讲上说可以用类似计数排序的方式写,但是我不会计数排序,代码暂略)
3.多重背包
两种思路:
1.转化为01背包问题
#include<bits/stdc++.h> #define N 101 using namespace std; int n,v,c[N],w[N],s[N],f[N]; int main(){ memset(f,0,sizeof(f)); scanf("%d%d",&n,&v); for(int i=1;i<=n;i++) scanf("%d%d%d",&c[i],&w[i],&s[i]); for(int i=1;i<=n;i++) for(int k=1;k<=s[i];k++) for(int j=v;j>=c[i];j--) f[j]=max(f[j],f[j-c[i]]+w[i]); printf("%d",f[v]); }
2.针对1的二进制优化:
对于有n件的物品,可以将其分为1、2、4...2^n件物品。
模板:洛谷P1776
(怎么加个二进制优化就变成了绿题)
#include<bits/stdc++.h> #define N 100001 #define M 40001 using namespace std; int n,v,c[N],w[N],s[N],f[N]; int main(){ memset(f,0,sizeof(f)); scanf("%d%d",&n,&v); for(int i=1;i<=n;i++) scanf("%d%d%d",&w[i],&c[i],&s[i]); for(int i=1;i<=n;i++){ int t=1; bool test=true; while(t){ int ci=c[i]*t,wi=w[i]*t; for(int j=v;j>=ci;j--) f[j]=max(f[j],f[j-ci]+wi); if(!test) break; s[i]-=t,t*=2; if(s[i]<=t) t=s[i],test=false; } } printf("%d",f[v]); }
感觉写得不太好看
3.单调队列优化
等学到单调队列动态规划再写(鸽了)
4.混合背包
原来混合背包也算绿题?
洛谷P1833:
#include<bits/stdc++.h> #define N 10001 #define M 1001 using namespace std; char ch; int h1,h2,t1,t2,n,v,c[N],w[N],s[N],f[M]; int main(){ memset(f,0,sizeof(f)); scanf("%d%c%d%d%c%d%d",&h1,&ch,&t1,&h2,&ch,&t2,&n); v=60*(h2-h1); if(t2>=t1) v+=(t2-t1); else v-=(t1-t2); for(int i=1;i<=n;i++) scanf("%d%d%d",&c[i],&w[i],&s[i]); for(int i=1;i<=n;i++){ if(!s[i]) for(int j=c[i];j<=v;j++) f[j]=max(f[j],f[j-c[i]]+w[i]); else{ int t=1; bool test=true; while(true){ int ci=c[i]*t,wi=w[i]*t; for(int j=v;j>=ci;j--) f[j]=max(f[j],f[j-ci]+wi); if(!test) break; s[i]-=t,t*=2; if(t>=s[i]) t=s[i],test=false; } } } printf("%d",f[v]); }
5.多维背包
限制条件增加时,状态转移方程维度也增加即可。
所有维度均要逆向循环避免重复。
#include<bits/stdc++.h>//二维背包,洛谷P1855 #define N 201 using namespace std; int n,m,t,c1[N],c2[N],f[N][N]; int main(){ scanf("%d%d%d",&n,&m,&t); for(int i=1;i<=n;i++) scanf("%d%d",&c1[i],&c2[i]); for(int i=1;i<=n;i++) for(int j=m;j>0;j--){ if(j<c1[i]) break; for(int k=t;k>0;k--) if(k>=c2[i]) f[j][k]=max(f[j][k],f[j-c1[i]][k-c2[i]]+1); else break; } printf("%d",f[m][t]); }
6.分组背包
本质和01背包类似,关键是如何实现“每组中只能选一件”。
01背包可以理解为每组物品有放或不放两种选择,分组背包可以理解为每组物品有 k [ i ] + 1 种选择。
伪代码:
for 1...k (循环组数)
for v...0 (循环空间)
for 1...k [ i ] (循环组中每个个体)
状态转移方程
先循环空间再循环组中个体,使得每次在处理一组中不同物品时,都只更改了一个 f [ v ] 的值,实现了每组中只选一件
(感觉还需要再理解一下)
模板:
洛谷U280369
#include<bits/stdc++.h> #define N 10001 using namespace std; int main(){ int n,v,cnt=0,s[N],c[N],w[N],f[N]; scanf("%d%d",&n,&v); for(int i=1;i<=n;i++){ scanf("%d",&s[i]); for(int j=1;j<=s[i];j++){ cnt++; scanf("%d%d",&c[cnt],&w[cnt]); } } int t=0; for(int i=1;i<=n;i++){ for(int j=v;j>0;j--){ for(int k=1;k<=s[i];k++){ t++; if(j>=c[t]) f[j]=max(f[j],f[j-c[t]]+w[t]); } t-=s[i]; //重新从本组开始循环 } t+=s[i]; //从下一组开始循环 } printf("%d",f[v]); }
洛谷P1757:
采用链式前向星的方法存储了组(还是链表?)还是不太习惯这种存图
#include<bits/stdc++.h> #define N 1001 using namespace std; int n,m,c[N],w[N],f[N],cnt=0; //类似前向星 int head[N],to[N]; //head[i]表示第i组第一个 int main(){ //to[i]表示第i个下一个同组 memset(head,-1,sizeof(head)); memset(to,-1,sizeof(to)); memset(f,0,sizeof(0)); scanf("%d%d",&m,&n); for(int i=1;i<=n;i++){ int t; scanf("%d%d%d",&c[i],&w[i],&t); if(head[t]!=-1) to[i]=head[t]; head[t]=i; cnt=max(cnt,t); } for(int i=1;i<=cnt;i++) for(int j=m;j>0;j--){ int k=head[i]; do{ if(j>=c[k]) f[j]=max(f[j],f[j-c[k]]+w[k]); k=to[k]; }while(k!=-1); } printf("%d",f[m]); }