(DP练习记录
背包dp
普通的01背包
#include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int maxn=1005; int w[maxn],c[maxn],f[maxn]; int main() { int i,j,T,N,V; scanf("%d",&T); while (T--) { memset(f,0,sizeof(f)); scanf("%d%d",&N,&V); for (i=1;i<=N;i++) scanf("%d",&w[i]); for (i=1;i<=N;i++) scanf("%d",&c[i]); for (i=1;i<=N;i++) for (j=V;j>=c[i];j--) f[j]=max(f[j],f[j-c[i]]+w[i]); printf("%d\n",f[V]); } return 0; }
题意:有个人想抢银行。有n个银行,抢劫银行i可以得到银行的储蓄金额c[i],并且有w[i]的概率被捕。给出概率p,要求出在被抓概率小于p的情况下,能够得到的最大金额数。
分析:01背包(恰好装满背包)。把银行储蓄总和当作背包容量V,每个银行的储蓄当作体积c,不被抓住的概率当作价值w。初始化f[0]=1,其余为0。状态转移方程为f[j]=max(f[j],f[j-c[i]]*(1.0-w[i]))。求出的f[j]代表恰好装满容量为j的背包所不被抓住的最大概率。
#include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int maxv=10005; const int maxn=105; double w[maxn],f[maxv]; int c[maxn]; int main() { int i,j,T,N,V; double p; scanf("%d",&T); while (T--) { V=0; memset(f,0,sizeof(f)); scanf("%lf%d",&p,&N); for (i=1;i<=N;i++) { scanf("%d%lf",&c[i],&w[i]); V+=c[i]; //所有银行的总金为容量 } f[0]=1; for (i=1;i<=N;i++) for (j=V;j>=c[i];j--) { f[j]=max(f[j],f[j-c[i]]*(1.0-w[i])); //f记录获得j而不被抓住的最大概率值 } for (i=V;1-f[i]>=p;i--) continue; printf("%d\n",i); } return 0; }
题意:有N头牛,每头牛i有权值s[i]与f[i]。选出若干头牛,使得它们的s之和MS与t之和MT均不小于0,并且MS+MT最大。求出最大的MS+MT的值。
分析:01背包(并不是二维费用)。将s当作体积,f当作价值做01背包,最后遍历一遍找i+dp[i]的最大值。
注意s,f的范围为(-1000,1000),在dp过程中,将体积加上一个数使它非负。
当s大于0时从大到小循环,否则从小到大循环。
#include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int maxn=1005; const int lim=100000; int s[maxn],f[maxn],dp[200005]; int main() { int i,j,ans=0,N; scanf("%d",&N); memset(dp,128,sizeof(dp)); dp[lim]=0; for (i=1;i<=N;i++) scanf("%d%d",&s[i],&f[i]); for (i=1;i<=N;i++) { if (s[i]>=0) { for (j=2*lim;j>=s[i];j--) dp[j]=max(dp[j],dp[j-s[i]]+f[i]); } else { for (j=0;j<=2*lim+s[i];j++) dp[j]=max(dp[j],dp[j-s[i]]+f[i]); } } for (i=lim;i<=2*lim;i++) if (dp[i]>=0) ans=max(ans,i+dp[i]-lim); printf("%d\n",ans); return 0; }
题意:有两辆卡车,承重分别为C1,C2。有n件家具,每件家具i有重量w[i]。问最少需要运几趟能把家具运完。
分析:状态压缩+01背包。N<=10,所以可以用二进制表示家具的状态。
首先处理出那些状态能够由这两辆卡车一次运输完。
假设能够一次运输完的家具状态为st[i],将st[i]当作物品的体积,1为物品的价值(运输趟数+1),有状态转移方程dp[st[i]|j]=min(dp[st[i]|j],dp[j]+1)
#include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int maxn=1050; int c[12],vis[maxn],st[maxn],dp[maxn]; int main() { int i,j,k,t,T,N,C1,C2,cnt,sum,inf; scanf("%d",&T); for (t=1;t<=T;t++) { scanf("%d%d%d",&N,&C1,&C2); memset(st,0,sizeof(st)); memset(dp,127,sizeof(dp)); inf=dp[0]; dp[0]=0; cnt=0; for (i=0;i<N;i++) scanf("%d",&c[i]); for (i=1;i<(1<<N);i++) { memset(vis,0,sizeof(vis)); vis[0]=1; sum=0; for (j=0;j<N;j++) { if ((i>>j)&1) { sum+=c[j]; for (k=C1;k>=c[j];k--) if (vis[k-c[j]]) vis[k]=1; } } if (sum<=C1+C2) { for (j=0;j<=C1;j++) if (vis[j] && sum-j<=C2) { st[++cnt]=i; break; } } } for (i=1;i<=cnt;i++) { for (j=(1<<N)-1;j>=0;j--) if ((st[i]&j)==0 && dp[j]!=inf) dp[st[i]|j]=min(dp[st[i]|j],dp[j]+1); } printf("Scenario #%d:\n%d\n\n",t,dp[(1<<N)-1]); } return 0; }
完全背包
题意:一个人带着N种硬币买一件价值为T的东西。第i种硬币的面值为V[i],数量为C[i]。老板找零时可以使用这N种硬币,并且老板拥有无限数量的硬币。问此人付出的硬币数和找零得到的硬币数之和的最小值。
分析:完全背包+多重背包。首先是完全背包,将老板找零i元所需的最小硬币数f[i]求出;然后是多重背包,将此人付出i元所需的最小硬币数dp[i]求出;最后只需求(f[i]+dp[i+T])的最小值即可。
需要注意的是f与dp数组的大小设置。f的大小应该设置为maxV*maxV。证明如下:
如果找零大于maxV*maxV,说明找零了超过maxV个硬币。
假设硬币有k个(k>maxV),硬币序列为a1,a2,a3.....,ak。那么就有序列a1,a1,a2,a1,a2,a3,......,a1,a2,...ak这k个子序列。
由于硬币数最少,那么这k个子序列的元素和均不能被maxV整除(否则可以使用更少的面值为maxV的硬币替代)。
所以这k个子序列模maxV的余数只可能为0,1,2......,maxV-1这maxV种。由于k>maxV,必然有两个序列的余数相同,于是他们的差被maxV整除。于是这中间一段序列便可以被更少的maxV硬币替代,从而使得硬币数比k小。
(当然其实也可以不思考,凭感觉就可以了。。。。WA了再改大一点。。。比如我就是。。。
#include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int maxT=10002; const int maxV=122; const int maxN=102; int dp[maxT+maxV*maxV],f[maxV*maxV],c[15*maxN],v[15*maxN],C[maxN],V[maxN]; int main() { int i,j,N,T,cnt,inf,lim,ans; while (scanf("%d%d",&N,&T)!=EOF) { lim=0; cnt=0; memset(f,60,sizeof(f)); memset(dp,60,sizeof(dp)); inf=dp[0]; f[0]=0; dp[0]=0; for (i=1;i<=N;i++) scanf("%d",&C[i]); for (i=1;i<=N;i++) scanf("%d",&V[i]); for (i=1;i<=N;i++) { lim=max(lim,C[i]); for (j=1;(j<<1)<=V[i];j<<=1) { c[++cnt]=j*C[i]; v[cnt]=j; } j--; if (j!=V[i]) { c[++cnt]=(V[i]-j)*C[i]; v[cnt]=V[i]-j; } } for (i=1;i<=N;i++) for (j=C[i];j<=lim*lim;j++) f[j]=min(f[j],f[j-C[i]]+1); for (i=1;i<=cnt;i++) for (j=T+lim*lim;j>=c[i];j--) dp[j]=min(dp[j],dp[j-c[i]]+v[i]); ans=inf; for (i=0;i<=lim*lim;i++) ans=min(ans,dp[i+T]+f[i]); if (ans!=inf) printf("%d\n",ans); else printf("-1\n"); } return 0; }
题意:有N个课程,M个单位的时间。在第i个课程里投入j个单位时间能够得到A[i][j]的收益。问能够得到的最大收益是多少。
分析:分组背包问题,每个课程为一个组,投入不同的时间相互冲突。将j看作体积,A[i][j]看作价值,做分组背包。
#include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int maxn=105; int dp[maxn],A[maxn][maxn]; int main() { int i,j,k,N,M; while (scanf("%d%d",&N,&M)!=EOF && (N!=0 || M!=0)) { memset(dp,0,sizeof(dp)); for (i=1;i<=N;i++) for (j=1;j<=M;j++) scanf("%d",&A[i][j]); for (i=1;i<=N;i++)//分组背包 for (j=M;j>=1;j--) for (k=1;k<=j;k++) dp[j]=max(dp[j],dp[j-k]+A[i][k]); printf("%d\n",dp[M]); } return 0; }
题意:有N个地点,每个地点有怪物,击杀第i个地点的怪物需花费时间t[i],可获得金钱g[i]。可以在一个地点和与它链接的地点之间移动。问得到M单位金钱所需花费的最小时间。
分析:分组背包,优先队列模拟01背包。
首先运用并查集,将所有地点分割为若干个连通块,每个连通块为一组做分组背包就是答案了。
但是T<=1e9,无法开1e9的数组,不能直接按照常规做法进行01背包。
使用优先队列模拟01背包的过程。每次选出当前获得金钱最多的状态进行更新,对于这个状态,由选或不选更新出两个新状态,再继续更新。(感觉有点像堆优化dij)
这样的时间复杂度是2^N,N<=50,需要剪枝。其中一个剪枝是,若两个状态i,j,T[i]<T[j]并且G[i]>G[j],显然不需要继续考虑j状态。
#include<cstdio> #include<algorithm> #include<cstring> #include<queue> using namespace std; const int INF=0x7fffffff; const int maxn=52; int fa[maxn],t[maxn],g[maxn]; struct block { int t,g; block(int tt,int gg) { t=tt;g=gg; } bool operator < (const block &x) const { if (g==x.g) return t>x.t; return g<x.g; //价值大的排在前面,同价值情况时间时间短的排在前面 } }; int find(int x) { if (x==fa[x]) return x; return fa[x]=find(fa[x]); } priority_queue<block> q1,q2; int main() { int n,m,i,j,tt,T,k,c,mi,ans; scanf("%d",&T); for (tt=1;tt<=T;tt++) { scanf("%d%d",&n,&m); for (i=1;i<=n;i++) fa[i]=i; ans=INF; for (i=1;i<=n;i++) { scanf("%d%d%d",&t[i],&g[i],&k); while (k--) { scanf("%d",&c); fa[find(i)]=find(c); } } for (i=1;i<=n;i++) { if (fa[i]!=i) continue; while (!q1.empty()) q1.pop(); while (!q2.empty()) q2.pop(); q1.push(block(0,0)); for (j=1;j<=n;j++) { if (find(j)!=i) continue; while (!q1.empty()) { block x=q1.top(); q1.pop(); if (x.g>=m)//当此时价值已满足要求 { ans=min(ans,x.t);//更新答案 continue; } if (x.t>=ans) continue;//当此时时间大于当前最优答案,不再考虑 q2.push(x);//不选择该物品 x.t+=t[j]; x.g+=g[j]; if (x.g>=m) { ans=min(ans,x.t); continue; } if (x.t>=ans) continue; q2.push(x);//选择该物品 } mi=INF; while (!q2.empty()) { block x=q2.top(); q2.pop(); if (x.t>=mi) continue;//若价值较小并且时间长,则略过 q1.push(x);//复制至q1 mi=x.t;//更新最小时间 } } } if (ans==INF) printf("Case %d: Poor Magina, you can't save the world all the time!\n",tt); else printf("Case %d: %d\n",tt,ans); } return 0; }
题意:有n个工作的集合,每个集合中有若干项工作。这些集合有三类,0类要求在这些工作中至少选取一个,1类要求至多选取一个,2类没有要求。每个工作i完成需要时间c[i],完成后获得快乐值g[i]。问在工作时间不超过T的情况下,满足所有要求能够取得的最大快乐值。
分析:多种背包混合。
定义数组dp[i][j],代表计算完前i个工作集合后,在工作时间不超过j的情况下能够取得的最大快乐值。初始化dp为负无穷,dp[0][]为0。
0类:至少选取一个。状态转移方程dp[i][j]=max(dp[i][j],dp[i-1][j-c[k]]+g[k],dp[i][j-c[k]]+g[k])
1类:至多选取一个。要先将dp[i-1]复制到dp[i],状态转移方程dp[i][j]=max(dp[i][j],dp[i-1][j-c[k]]+g[k])
2类:没有要求。直接01背包,不过要先将dp[i-1]复制到dp[i],状态转移方程dp[i][j]=max(dp[i][j],dp[i][j-c[k]]+g[k])
#include<cstdio> #include<algorithm> #include<cstring> using namespace std; const int maxn=102; int dp[maxn][maxn],c[maxn],g[maxn]; int main() { int i,j,k,n,m,s,T; while (scanf("%d%d",&n,&T)!=EOF) { memset(dp,128,sizeof(dp)); for (j=0;j<=T;j++) dp[0][j]=0; for (i=1;i<=n;i++) { scanf("%d%d",&m,&s); for (j=1;j<=m;j++) scanf("%d%d",&c[j],&g[j]); if (s==0)//至少做一项 { for (k=1;k<=m;k++) for (j=T;j>=c[k];j--) dp[i][j]=max(dp[i][j],max(dp[i-1][j-c[k]]+g[k],dp[i][j-c[k]]+g[k])); } else if (s==1)//至多做一项 { for (j=0;j<=T;j++) dp[i][j]=dp[i-1][j]; for (k=1;k<=m;k++) for (j=T;j>=c[k];j--) dp[i][j]=max(dp[i][j],dp[i-1][j-c[k]]+g[k]); } else if (s==2)//01背包 { for (j=0;j<=T;j++) dp[i][j]=dp[i-1][j]; for (k=1;k<=m;k++) for (j=T;j>=c[k];j--) dp[i][j]=max(dp[i][j],dp[i][j-c[k]]+g[k]); } } //printf("ans: "); if (dp[n][T]<0) printf("-1\n"); else printf("%d\n",dp[n][T]); } return 0; }