之前学了dp没有好好看一遍背包九讲,今天把背包九讲过一遍,供之后自己看方便一些。
一. 01背包
题目链接:https://www.acwing.com/problem/content/2/
n,V<=1000
这个没什么好说的,加滚动数组,复杂度O(nV),代码如下:
#include<cstdio> #include<algorithm> using namespace std; const int maxn=1e3+5; int n,V,dp[maxn],v[maxn],w[maxn]; int main(){ scanf("%d%d",&n,&V); for(int i=1;i<=n;++i) scanf("%d%d",&v[i],&w[i]); for(int i=1;i<=n;++i) for(int j=V;j>=v[i];--j) dp[j]=max(dp[j],dp[j-v[i]]+w[i]); printf("%d ",dp[V]); return 0; }
要注意的是,如果题目限制一定要装满,那么应将dp数组初始化为负无穷,只将dp[0]初始化0,此时dp[i]的定义是背包大小为i装满的最大价值,其余都一样,答案是dp[V]。
二. 完全背包
题目链接:https://www.acwing.com/problem/content/3/
n,V<=1000
只用把01背包的遍历j的顺序从逆序改成顺序即可,复杂度O(nV)。代码如下:
#include<cstdio> #include<algorithm> using namespace std; const int maxn=1e3+5; int n,V,v[maxn],w[maxn],dp[maxn]; int main(){ scanf("%d%d",&n,&V); for(int i=1;i<=n;++i) scanf("%d%d",&v[i],&w[i]); for(int i=1;i<=n;++i) for(int j=v[i];j<=V;++j) dp[j]=max(dp[j],dp[j-v[i]]+w[i]); printf("%d ",dp[V]); return 0; }
同样的,如果题目限制要装满,将dp数组初始化为负无穷,仅将dp[0]=0。
三. 多重背包
1. 多重背包I(O(nVs))
题目链接:https://www.acwing.com/problem/content/4/
n种物品,v[i],w[i],s[i]分别表示物品i的体积、价值和数量,背包大小V,求最大价值。n,V,s<=100。
在01背包基础上加一层循环表示物品i选几个,即dp[j]=max(dp[j] , dp[j-k*v[i]]+k*w[i]),k=1..s[i]。
#include<cstdio> #include<algorithm> using namespace std; const int maxn=105; int n,V,v[maxn],w[maxn],s[maxn],dp[maxn]; int main(){ scanf("%d%d",&n,&V); for(int i=1;i<=n;++i) scanf("%d%d%d",&v[i],&w[i],&s[i]); for(int i=1;i<=n;++i) for(int j=V;j>=v[i];--j) for(int k=1;k<=s[i]&&k*v[i]<=j;++k) dp[j]=max(dp[j],dp[j-k*v[i]]+k*w[i]); printf("%d ",dp[V]); return 0; }
2. 多重背包II--二进制优化(O(nVlogs))
题目链接:https://www.acwing.com/problem/content/5/
题意和上面的一样,只是数据范围变了,n<=1000,V,s<=2000。
如果按照上一题做法,复杂度是4e9,妥妥的T飞。
首先,想到将每种物品拆分成s[i]个物品,01背包就能解决,那么就有1000×2000=2e6个物品,乘个V,4e9的复杂度,QAQ。。
接下来利用二进制的力量,把物品的数量s[i]分成k个数20、21...2k,k=log2s[i],那么用这几个数可以表示1到s[i]中的任意数,这样复杂度就降为O(nVlogs)。但要注意的是对于7,我们分解成1、2、7是没问题的,对于10,我们不能分解为1、2、4、8,因为这样表示的数可能超过10,那么这种情况我们将最后一个数定为s[i]-前面的和,即用1、2、4、3表示10。因为s[i]<=2000<2048,故最多用11个数就能表示s[i]。复杂度为2e7。
代码:
#include<cstdio> #include<algorithm> using namespace std; const int maxn=1005*11+5; int n,V,cnt,v[maxn],w[maxn],dp[maxn]; int main(){ scanf("%d%d",&n,&V); for(int i=1;i<=n;++i){ int vv,ww,ss; scanf("%d%d%d",&vv,&ww,&ss); for(int j=1;j<=ss;j*=2){ v[++cnt]=vv*j; w[cnt]=ww*j; ss-=j; } if(ss){ v[++cnt]=vv*ss; w[cnt]=ww*ss; } } for(int i=1;i<=cnt;++i) for(int j=V;j>=v[i];--j) dp[j]=max(dp[j],dp[j-v[i]]+w[i]); printf("%d ",dp[V]); return 0; }
3. 多重背包III(O(nV))
题目链接:https://www.acwing.com/problem/content/description/6/
题意仍然一样,数据范围变为n<=1000,V,s<=20000。此时上述二进制优化的O(nVlogs)做法也不能通过。此时考虑单调队列来优化,去掉复杂度中的logs。
多重背包的最原始的状态转移方程:
令 c[i] = min(num[i], j / v[i]) ,f[i][j] = max(f[i-1][j-k*v[i]] + k*w[i]) (1 <= k <= c[i]) 这里的 k 是指取第 i 种物品 k 件。
如果令 a = j / v[i] , b = j % v[i] 那么 j = a * v[i] + b。这里用 k 表示的意义改变, k 表示取第 i 种物品的件数比 a 少几件。
那么 f[i][j] = max(f[i-1][b+k*v[i]] - k*w[i]) + a*w[i] (a-c[i] <= k <= a)
可以发现,f[i-1][b+k*v[i]] - k*w[i] 只与 k 有关,而这个 k 是一段连续的(只要dp问题的状态方程能够表示成这样,就可以用单调队列来优化)。我们要做的就是求出 f[i-1][b+k*v[i]] - k*w[i] 在 k 取可行区间内时的最大值。所以我们可以按j模v[i]的余数来划分,分别用单调队列来求解。
AC代码:
#include<cstdio> #include<algorithm> using namespace std; const int maxn=1005; const int maxv=20005; int n,V,dp[maxv],q1[maxv],q2[maxv]; int head,tail; int main(){ scanf("%d%d",&n,&V); for(int i=1;i<=n;++i){ int v,w,s; scanf("%d%d%d",&v,&w,&s); for(int j=0;j<v;++j){ head=1,tail=0; for(int k=j,num=0;k<=V;k+=v,++num){ int tmp=dp[k]-num*w; while(head<=tail&&tmp>=q1[tail]) --tail; q1[++tail]=tmp; q2[tail]=num; while(num-q2[head]>s) ++head; dp[k]=q1[head]+num*w; } } } printf("%d ",dp[V]); return 0; }
四. 混合背包问题
题目链接:https://www.acwing.com/problem/content/7/
题意:前3类问题的综合,即一种物品可能是01背包,可能是完全背包,可能为多重背包。
只用将多重背包的O(nV)解法稍微改一下即可,即如果s=-1,令s=1,如果s=0,令s=V/v,其它情况一样。
AC代码:
#include<cstdio> #include<algorithm> using namespace std; const int maxn=1005; const int maxv=1005; int n,V,dp[maxv],q1[maxv],q2[maxv]; int head,tail; int main(){ scanf("%d%d",&n,&V); for(int i=1;i<=n;++i){ int v,w,s; scanf("%d%d%d",&v,&w,&s); if(s==-1) s=1; else if(s==0) s=V/v; for(int j=0;j<v;++j){ head=1,tail=0; for(int k=j,num=0;k<=V;k+=v,++num){ int tmp=dp[k]-num*w; while(head<=tail&&tmp>=q1[tail]) --tail; q1[++tail]=tmp; q2[tail]=num; while(num-q2[head]>s) ++head; dp[k]=q1[head]+num*w; } } } printf("%d ",dp[V]); return 0; }
五. 二维费用的背包问题
题目链接:https://www.acwing.com/problem/content/8/
题意:在01背包的基础上增加背包的最大承重M,每个物品有3个属性v,m,w,即其体积、重量和价值,每个物品只有1件。
思路:思路与01背包一样,仅需增加一维,用dp[i][j]表示空间为i、承重为j的背包的最大价值,然后加一层表示重量的循环即可,V和M均从大到小遍历。复杂度O(nVM)。
AC代码:
#include<cstdio> #include<algorithm> using namespace std; const int maxv=105; int n,V,M,dp[maxv][maxv]; int main(){ scanf("%d%d%d",&n,&V,&M); for(int i=1;i<=n;++i){ int v,m,w; scanf("%d%d%d",&v,&m,&w); for(int j=V;j>=v;--j) for(int k=M;k>=m;--k) dp[j][k]=max(dp[j][k],dp[j-v][k-m]+w); } printf("%d ",dp[V][M]); return 0; }
同理,二维费用的背包问题的完全背包和多重背包的求解也与前面类似。
六. 分组背包
题目链接:https://www.acwing.com/problem/content/9/
题意: n组物品,背包容积为V。每组物品有s[i]个物品,每组中最多选1个物品。问最大价值。
思路:与多重背包类似,只是每一组最多选一个物品,用3层循环即可。复杂度为O(nVs),没有优化方案。
AC代码:
#include<cstdio> #include<algorithm> using namespace std; const int maxn=105; int n,V,s[maxn],v[maxn][maxn],w[maxn][maxn],dp[maxn]; int main(){ scanf("%d%d",&n,&V); for(int i=1;i<=n;++i){ scanf("%d",&s[i]); for(int j=1;j<=s[i];++j) scanf("%d%d",&v[i][j],&w[i][j]); } for(int i=1;i<=n;++i) for(int j=V;j>=0;--j) for(int k=1;k<=s[i];++k) if(j>=v[i][k]) dp[j]=max(dp[j],dp[j-v[i][k]]+w[i][k]); printf("%d ",dp[V]); return 0; }
七. 有依赖的背包问题
题目链接:https://www.acwing.com/problem/content/description/10/
题意:有n个物品,背包容量为V。每个物品有3个属性,体积、价值和父结点。并规定如果选一个结点,必须选它的父结点。
思路:树上背包问题,树形dp+分组背包。dp[u][j]表示对于以u为根的子树来说,容量为j的背包的最大价值。假设v1,v2,v3是u的3个子结点,那么相当于有3组,每一组最多选择一种方案。通过递归得到所有dp[v1][j]的最大值,对v2,v3同理。那么转移方程为dp[u][j]=max(dp[u][j],dp[u][j-k]+dp[v1][k]),其中0<=k<=j,表示在子树v1中选择的总体积为k。
需要注意的是,我们选择了子节点,就必须选择当前节点,那么最后需要把父节点的位置空出来。(把所有已算完的体积更新一下,在里面加上父节点这一物品)描述有些抽象,详见代码吧。
复杂度O(nV^2)。
AC代码:
#include<cstdio> #include<algorithm> using namespace std; const int maxn=105; int n,V,cnt,root,head[maxn],v[maxn],w[maxn],dp[maxn][maxn]; struct node{ int v,nex; }edge[maxn]; void adde(int u,int v){ edge[++cnt].v=v; edge[cnt].nex=head[u]; head[u]=cnt; } void dfs(int u){ for(int i=head[u];i;i=edge[i].nex){ int v=edge[i].v; dfs(v); for(int j=V;j>=0;--j) for(int k=0;k<=j;++k) dp[u][j]=max(dp[u][j],dp[u][j-k]+dp[v][k]); } for(int j=V;j>=0;--j) if(j>=v[u]) dp[u][j]=dp[u][j-v[u]]+w[u]; else dp[u][j]=0; } int main(){ scanf("%d%d",&n,&V); for(int i=1;i<=n;++i){ int u; scanf("%d%d%d",&v[i],&w[i],&u); if(u==-1) root=i; else adde(u,i); } dfs(root); printf("%d ",dp[root][V]); return 0; }
八. 背包问题求方案数
题目链接:https://www.acwing.com/problem/content/description/11/
题意:在01背包的基础上询问取得最大价值的方案数。
思路:只需要添加一个数组num[j],表示容量为j的背包取到最优解时的方案数,num数组初始化为1。复杂度O(nV)。
AC代码:
#include<cstdio> #include<algorithm> using namespace std; const int maxn=1005; const int MOD=1e9+7; int n,V,dp[maxn],num[maxn]; int main(){ scanf("%d%d",&n,&V); for(int i=0;i<=V;++i) num[i]=1; for(int i=1;i<=n;++i){ int v,w; scanf("%d%d",&v,&w); for(int j=V;j>=v;--j){ if(dp[j]<dp[j-v]+w){ dp[j]=dp[j-v]+w; num[j]=num[j-v]; } else if(dp[j]==dp[j-v]+w){ num[j]+=num[j-v]; num[j]%=MOD; } } } printf("%d ",num[V]); return 0; }
九. 背包问题求具体方案
题目链接:https://www.acwing.com/problem/content/12/
题意:01背包,输出字典序最小的最优方案。
思路:首先我们从n到1号物品遍历,保证序号小的优先选择,从而保证字典序最小。然后从1到n遍历确定i是否被选。复杂度O(nV)。
AC代码:
#include<cstdio> #include<algorithm> using namespace std; const int maxn=1e3+5; int n,V,v[maxn],w[maxn],dp[maxn][maxn],ans[maxn]; int main(){ scanf("%d%d",&n,&V); for(int i=1;i<=n;++i) scanf("%d%d",&v[i],&w[i]); for(int i=n;i>=1;--i) for(int j=V;j>=0;--j) if(j>=v[i]) dp[i][j]=max(dp[i+1][j],dp[i+1][j-v[i]]+w[i]); else dp[i][j]=dp[i+1][j]; int tmp=V; for(int i=1;i<=n;++i) if(tmp>=v[i]&&dp[i][tmp]==dp[i+1][tmp-v[i]]+w[i]){ printf("%d ",i); tmp-=v[i]; } printf(" "); return 0; }