动态规划:常见的基础问题整理
写在前面:动态规划与分治有一定的类似之处,都是将原问题分解成子问题解决。但是,动态分解得到的子问题往往不是独立的,子问题之间可能共享相同的子问题;而分治的子问题相互独立互不影响。动态规划常用于求最优解的问题。
解决动态规划问题的关键点在于确定状态量和状态转移方程,并选择合适的复杂度范围。状态量要能完全表示出状态的特征,状态间的转移完全依赖于各个状态本身。
递推问题,利用了DP的思想。对于一个问题,如果它的解和它的子问题相关,则先处理子问题,再由子问题的解递推出当前问题的解。一个典型问题就是错排,有n对物品两两对应,但是现在要求全部不对应的排列方式有几种,F[n]=(n-1)*F[n-1]+(n-1)*F[n-2]
LIS问题,最长递增子序列。以F[i]来表示以第i个元素结尾的递增子序列的最长长度。F[i]=max{1,F[j]+1|aj<ai&&j<i},复杂度是O(n2)。基本模型可以考虑导弹的例题,原序列是有序的,在固定序列上求最长递增子序列。对于有多个属性的无需元素,可以先按某一属性排序。
对于求LIS长度,有O(nlgn)的方法,通过贪心维护子序列长度为i时,第i个元素的最小值.
int loc=0; for(int i=1;i<=n;i++){ if(nodes[i].wid>dp[loc]) dp[++loc]=nodes[i].wid; else *(lower_bound(dp+1,dp+loc+1,nodes[i].wid))=nodes[i].wid; }
还有一个拓展,求最长递增子序列的个数,使各个子序列能够覆盖整个区间。可以将该问题转化为求最长不递增子序列的长度。
LCS问题,最长公共子序列,有两个字符串s1和s2,求一个字符串s3同时是s1和s2的子串,同时长度最大。dp[i][j]表示到s1的第i个元素,到s2的第j个元素时的最长子串的长度。dp[i][0]=dp[0][j]=0;dp[i][j]=dp[i-1][j-1]+1|s1[i]==s2[j];dp[i][j]=max{dp[i-1][j],dp[i][j-1]}|s1[i]!=s2[j]
背包问题,在一个有容积限制或重量限制的背包中放入物品,物品拥有体积、重量和价值等属性,需要求一种满足背包限制的放置物品的方式,使背包中物品的价值和最大。
01背包,每种物品有且仅有一个,有权值和体积两个属性。只需要考虑每种物品选与不选这两种情况。dp[i][j]表示前i个物品占用j体积时的最大价值,有转移方程:dp[i][j]=max(dp[i-1][j],dp[i-1][j-c[i]]+w[i]),j>=c[i]。可以发现dp[i]-1[j]只可能对dp[i][x](x>=j)有影响,因此dp[i][j]可以覆盖dp[i-1][j],从而实现空间的优化。时间复杂度为O(VN)
for(int i=0;i<n;i++){ for(int j=v;j>=c[i];j--){ dp[j]=max(dp[j],dp[j-c[i]]+w[i]); } }
01背包的简单变形,要求最终重量恰好装满背包。状态量和转移方程还是一样的,但是状态初始值发生变化。dp[0][0]=0,dp[0][i]=-1表示当前状态不可到达。
完全背包,每种物品的数量没有限制。考虑枚举每种物品的个数,从而转化为01背包问题,但是复杂度太高。可以直接修改01背包第二层循环的方向,因为01背包要求每种物品要么选要么不选,而完全背包下dp[i][j]可能是由dp[i][j-w[i]]转移得来的,而dp[i][j-w[i]]对物品个数没有要求,可能已经拿过第i个物品。时间复杂度为O(VN)
for(int i=0;i<n;i++){ for(int j=c[i];j<=v;j++){ dp[j]=max(dp[j],dp[j-c[i]]+w[i]); } }
多重背包,每种物品可以有多个但有个数限制。尝试用分组的方式转化为01背包,若第i中物品为数量k[i],那么新的每一个物品价值w[i]*1、w[i]*2、...,体积c[i]*1、c[i]*2、...时间复杂度变为O(V*∑log2(k[i]))。对于部分物品,若num[i]*w[i]>=v,可以认为这种物品是无限的,以完全背包处理
for(int i=1;i<=n;i++){ int d=1,k=num[i]; while(k-d>0){ k-=d; item[++loc].w=w[i]*d; item[loc].v=v[i]*d; d*=2; } item[++loc].w=w[i]*d; item[loc].v=v[i]*d; } for(int i=1;i<=loc;i++){ for(int j=v;j>=item[i].w;j--){ dp[j]=max(dp[j],dp[j-item[i].w]+item[i].v); } }
使用单调队列,可以将多重背包的复杂度优化为O(VN)。这个做法是学习了《挑战程序设计实践》。这里先不讲解了(说实话对于推导过程还是有点懵...),只贴代码。但是多重背包这样的做法是很强的.例题:POJ1276,POJ1742,POJ1014,POJ3260
//POJ1276 #include<stdio.h> #include<string.h> int n,m,num[15],val[15],dp[100005]; int main(){ while(scanf("%d",&m)!=EOF){ scanf("%d",&n); for(int i=0;i<n;i++){ scanf("%d%d",&num[i],&val[i]); } memset(dp,-1,sizeof(dp)); dp[0]=0; for(int i=0;i<n;i++){ for(int j=0;j<=m;j++){ if(dp[j]>=0) dp[j]=num[i]; else if(j<val[i]||dp[j-val[i]]<=0) dp[j]=-1; else dp[j]=dp[j-val[i]]-1; } } for(int i=m;i>=0;i--){ if(dp[i]>=0){ printf("%d\n",i); break; } } } return 0; }
//POJ3260 #include<stdio.h> #include<string.h> #include<algorithm> using namespace std; const int maxn=30005,inf=0x3f3f3f3f; int n,t,d1[maxn],d2[maxn],deq[maxn],deqv[maxn],v[105],c[105]; int main(){ while(scanf("%d%d",&n,&t)!=EOF){ memset(d1,0x3f,sizeof(d1)); memset(d2,0x3f,sizeof(d2)); int maxv=0; for(int i=0;i<n;i++){ scanf("%d",&v[i]); maxv=max(maxv,v[i]); } for(int i=0;i<n;i++){ scanf("%d",&c[i]); } maxv*=maxv; d1[0]=d2[0]=0; for(int i=0;i<n;i++){ for(int a=0;a<v[i];a++){ int s=0,t=0; for(int j=0;j*v[i]+a<=t+maxv;j++){ int val=d1[j*v[i]+a]-j; while(s<t&&val<=deqv[t-1]) t--; deq[t]=j; deqv[t++]=val; d1[j*v[i]+a]=deqv[s]+j; if(deq[s]==j-c[i]) s++; } } } for(int i=0;i<n;i++){ for(int j=v[i];j<=t+maxv;j++){ d2[j]=min(d2[j],d2[j-v[i]]+1); } } int ans=inf; for(int i=t;i<=t+maxv;i++){ if(d1[i]==inf||d2[i-t]==inf) continue; ans=min(ans,d1[i]+d2[i-t]); } if(ans==inf) printf("-1\n"); else printf("%d\n",ans); } }
背包经常会以变体的形式考察,不会是可以直接处理的背包问题,需要对数据进行一定处理之后才能变为背包问题,主要是一个思维的过程。例题洛谷P1064
多重部分和问题(感觉也算是一种特殊的多重背包),POJ1276,POJ1742。可以以O(n*K)的复杂度解决
//POJ1742 #include<stdio.h> #include<string.h> int n,m,num[15],val[15],dp[100005]; int main(){ while(scanf("%d",&m)!=EOF){ scanf("%d",&n); for(int i=0;i<n;i++){ scanf("%d%d",&num[i],&val[i]); } memset(dp,-1,sizeof(dp)); dp[0]=0; for(int i=0;i<n;i++){ for(int j=0;j<=m;j++){ if(dp[j]>=0) dp[j]=num[i]; else if(j<val[i]||dp[j-val[i]]<=0) dp[j]=-1; else dp[j]=dp[j-val[i]]-1; } } for(int i=m;i>=0;i--){ if(dp[i]>=0){ printf("%d\n",i); break; } } } return 0; }
划分数,有n个无区别的物品,将它们划分为不超过m组,求总的划分方法数。POJ1664
//POJ1664 #include<stdio.h> int m,n,dp[100][100]; int main(){ int t; scanf("%d",&t); while(t--){ scanf("%d%d",&m,&n); dp[0][0]=1; for(int i=1;i<=n;i++){ for(int j=0;j<=m;j++){ if(j>=i) dp[i][j]=dp[i-1][j]+dp[i][j-i]; else dp[i][j]=dp[i-1][j]; } } printf("%d\n",dp[n][m]); } }
多重集组合数,有n种物品,第i种物品有ai个,不同种类的物品可以互相区分但相同种类的无法区分。从这些物品中取出m个的话,有多少种取法?POJ3046
//POJ3046 #include<stdio.h> int n,m,a,b,num[1005],dp[1005][100005],mod=1000000; int main(){ scanf("%d%d%d%d",&n,&m,&a,&b); for(int i=1;i<=m;i++){ int c; scanf("%d",&c); num[c-1]++; } for(int i=0;i<=n;i++){ dp[i][0]=1; } for(int i=0;i<n;i++){ for(int j=1;j<=m;j++){ if(j-1-num[i]<0) dp[i+1][j]=(dp[i][j]+dp[i+1][j-1])%mod; else dp[i+1][j]=(dp[i][j]+dp[i+1][j-1]-dp[i][j-1-num[i]]+mod)%mod; } } int ans=0; for(int i=a;i<=b;i++){ ans=(ans+dp[n][i])%mod; } printf("%d\n",ans); }