动规大总结
Posted manmanjiangqwq
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动规大总结相关的知识,希望对你有一定的参考价值。
对前几天的动规类型做一个小分析,会简单介绍类型并指出重点和易错点。
关于总结的博客另外推荐几个大佬,总结得非常好: 自为风月马前卒大佬,FlashHu大佬。
状压DP
状压DP主要适用于数据范围很小以至于可以直接把当前状态作为下标的题目。
“数组的定义及状态之间的转移方程”是答题的关键,另外根据题目的特殊条件做特殊处理。
因为状压DP没有固定模板这里就不放代码了,放个枚举子集的代码。
for循环枚举T的所有子集的代码:
for(int i=T;i;i=(i-1)&T){ //其中每个i为T的一个子集。 }
单调队列优化DP
算是常见的优化类型,一般形式为:f[i]=max{f[j]+s[j]}。(j<i,i-j<=k)(这里只考虑max的情况,min同理)
很明显如果没有i-j<=k的限制,我们就可以一路取f[i]+s[i]的max,直接赋值即可。
而对于这个限制,我们只需要维护一个递减的单调队列,左端点存有最优解且控制距离不超过k,右端点加入新值同时保持递减的性质。
下面是模板:
for(int i=1;i<=n;i++){ while(l<=r&&i-p[l]>k) l++; f[i]=f[p[l]]+s[p[l]]; while(l<=r&&f[i]+s[i]>=f[p[r]]+s[p[r]]) r--; p[++r]=i; } //p[l~r]是单调队列,存的是递减的f[i]+s[i]的下标 //l是左端点,r是右端点
如果i和j有特殊的限制,比如i-j必须是m的倍数,我们就可以将j拆为am+b,对每个b都做一个单调队列去求即可。比如宝物筛选。
动态DP
这个就很有意思了,具体的思想和做法看这篇博客吧,写得非常好。
一般动态DP都需要定义一种矩阵运算为C[i][j]=max{A[i][k]+B[k][j]},然后把矩阵放入线段树中进行一系列操作。
下面是部分模板:
struct Num{ int p[3][3]; Num(){ memset(p,0,sizeof(p)); p[2][2]=-1e8; //这里p的取值由题目决定 } inline Num operator*(Num b){ Num c; for(int i=1;i<=2;i++) for(int j=1;j<=2;j++) for(int k=1;k<=2;k++) c.p[i][j]=Max(c.p[i][j],p[i][k]+b.p[k][j]); return c; } }b[N*4]; Num Query(int x,int l,int r,int L,int R){ if(L<=l&&r<=R) return b[x]; int mid=(l+r)>>1; if(mid>=R) return Query(x<<1,l,mid,L,R); else if(mid<L) return Query(x<<1|1,mid+1,r,L,R); else return Query(x<<1,l,mid,L,R)*Query(x<<1|1,mid+1,r,L,R); }
树状数组优化DP
这个没什么好讲的,树状数组可以logn求前缀的最大值及前缀和。
一维一般用不到,二维就可以log2n求二维前缀max和二维前缀和。(废话)
斜率优化DP
MashiroSky大佬的博客写得非常清楚,其中数形结合的思想对于斜率优化非常重要,建议把这篇文献全文背诵认真看一遍。
具体的做法我以[HNOI2008]玩具装箱举例子(以下步骤摘抄自我的博客):
首先定义f[x]为将前x件玩具刚好全部放入容器中的最小费用。
则状态转移方程即为f[i]=min{f[j]+(i-(j+1)+sum[i]-sum[j]-L)2}。(j<=i)
使p[i]=sum[i]+i,使L++,原方程可转化为f[i]=min{f[j]+(p[i]-p[j]-L)2}。
展开括号,并将与j无关项移出大括号,得:f[i]=min{f[j]+p[j]2+2p[j]*L-2p[j]*p[i]}+p[i]2+L2-2p[i]*L。
然后我们就可以设b=f[i],k=p[i],x=2p[j],y=f[j]+p[j]2+2p[j]*L。(这里我们忽略大括号外面的项,计算时加上即可)
根据数形结合的思想,这题就变成了:图上有若干个点(x,y),每次有两个操作:
1.加入一个新点,这个点的x要大于之前的所有点。
2.给定一个斜率k,求过图上的点且斜率为k的直线的最小截距。
然后就可以用下凸包维护了。(最大截距就用上凸包)
说起来好像大部分斜率优化DP都是这样的,但是这题的k也是递增的,所以凸包用单调队列即可O(n)。
当然不同的题目有不同的方程有不同的特点,如果k不是递增的就只能二分时间复杂度为O(nlogn),如果x也不是递增的好像就要用平衡树维护了。
特别注意求斜率时要判断两个点的x坐标是否相等,相等的话定为无穷大还是无穷小等等。
下面给出这题的核心代码:
int n,l,r; ll L,a[N],p[N],f[N]; struct nod{ ll x,y; }b[N],A; double Qiuk(nod X,nod Y){ return 1.0*(X.y-Y.y)/(X.x-Y.x); } int main(){ ...... l=1,r=0; b[++r].x=0; b[r].y=0; for(int i=1;i<=n;i++){ while(l<r&&Qiuk(b[l+1],b[l])<=p[i]) l++; f[i]=b[l].y-p[i]*b[l].x+(p[i]-L)*(p[i]-L); A.x=2*p[i]; A.y=f[i]+p[i]*p[i]+2*p[i]*L; while(l<r&&Qiuk(A,b[r])<Qiuk(b[r],b[r-1])) r--; b[++r]=A; } printf("%lld ",f[n]); return 0; }
决策单调性优化DP
首先决策单调性的定义是:对于j<k,如果当前点i的决策点选k比选j好,则后面的点的决策点选k都比选j好(这是决策单调递增,决策单调递减同理),这样就导致1~n的最佳决策点呈单调递增(其实是单调不下降)。
至于决策单调性的证明,应该要用数学归纳法(MashiroSky大佬的博客有讲),当然打表也可以。
这里我主要讲下具体做法:
每次做完一个点i,我们考虑把i当做最佳决策点给后面的点,由于决策单调递增,则每个点的最佳决策点应该为:
0 0 2 3 3(类似这样的形式)
一步一步来,首先所有的决策点都是0:
0 0 0 0 0。
然后f[1]求出来了,再二分1比0更优的点,决策点就变成了:
0 0 0 1 1。
再然后f[2]求出来了,先比较1的最左边的点,发现2更优,再在0的区域二分:
0 0 2 2 2。
再f[3]求出来了,在2的区域二分:
0 0 2 3 3。
再f[4]求出来了,二分后发现4在最后一点都没有3优,于是决策点还是:
0 0 2 3 3。
最后f[5]求出来了。
看起来需要区间修改,其实用个单调栈或单调队列维护就行了。注意边界的一些细节。
本人的代码可读性极差,这里我就不放出来误导大家了,你们可以自己去找一些标准的模板。
数位DP
介绍一篇很好的博客。数位dp顾名思义,就是针对每个数位做dp。
本人比起递推dp更习惯记忆化搜索,需要注意的是dp数组下标应该存哪些变量才能保证无后效性。
而数位dp的代码大致相同,下面是核心的一个函数:
int Dfs(int x,int lim){ if(!x) return 1; if(!lim&&f[x]!=-1) return f[x]; int p=lim?a[x]:9,sum=0; for(int i=0;i<=p;i++){ if(...) continue ;//限制条件 sum+=Dfs(x-1,lim&&i==p); } if(!lim) f[x]=sum; return sum; } //a[x]是从右往左数第x位上的数字 //lim表示是不是Dfs前面的数字都是a[],如果是,则要限制当前这位的数字的取值范围 //当然真正的代码里肯定不只两个参数,也不只一维数组,具体情况具体分析
结束语
本人的水平不高,文笔也不好,但如果能帮助到你,那就太好了。
如果有什么错误或是有什么问题,欢迎评论问我,我会一一订正或解答的。
感谢你看到这里。
以上是关于动规大总结的主要内容,如果未能解决你的问题,请参考以下文章