【易懂】斜率DP

前言

首先此篇文章是为低年级的朋友准备的,不涉及什么深奥的知识比如线性规划之类的仔细看,不要以为自己学不会,看不懂,只要你会DP并打过一些题目而且会单调队列优化DP,斜率DP离你就不远了~~~。这篇文章也是在我领悟了斜率DP不久写的,如果本文有什么不严谨的地方,欢迎指出!!!在此推荐一个大佬的BLOG,https://www.cnblogs.com/Xing-Ling/p/11210179.html,讲得很详细,如果你理解能力稍微强一点,可以看这篇文章,本文取自这篇博客的精华外加自己感受,换了一个简单点的题目方便理解。

斜率DP介绍

“斜率DP”,顾名思义就是通过斜率来优化DP,斜率式子\(\frac{y_1-y_2}{x_1-x_2}\)表示\(x_1,y_1\)\(x_2,y_2\)两点的斜率。可以用斜率优化的式子通常可以写成\(f[i]=min/max(f[j]+P(i)*Q(j))+H(i)\)\(P\)\(H\)是只与\(i\)有关的函数,\(Q\)是只与\(j\)有关的函数,单调队列优化解决不了的就在于既包含\(i\)又包含\(j\)的这一项\(P(i)*Q(j)\),于是诞生了斜率DP这么个东西,当然它对这个式子也有一个特殊要求\(P\)\(Q\)中至少有一个是单调递增/递减的(如果没有,据说可以用一些高级算法维护)。

  1. 如果有一个是单调的,可以优化到\(O(n \log_2 n)\)
  2. 如果有两个是单调的,也就是说满足决策单调性(以上BLOG有讲),可以优化到\(O(n)\)

至于为什么,请听下面分析。

理解斜率DP

这里以NOIP2010 四校联考模拟一 city为例。题目大意:有\(n\)个点,从第\(i\)个点到第\(j\)个点(\(i<j\))的费用为\((j-i)*a[i]+b[j]\),求从第\(1\)个点到第\(n\)个点的最小费用。这题描述很简单。

那么由题列出的状态转移方程\(f[i]=min(f[j]+j*a[i]+b[j])-i*a[i](i<j)\),可能看到这你有点疑问吧,为了让这题可做,我们从\(n\)~\(1\)枚举\(i\),至于为什么,学会了你可以自己顺着试试。我们把只含\(i\)的项提了出来,因为在每次算\(f[i]\)时是不变的。普通做法的时间复杂度显然是\(O(n^2)\)的,显然是过不了的。

\(j,k(0<i<k<j \leq n)\)\(i\)决策点一定要注意这里的大小关系),也就是可以转移到\(i\)的两点,且决策点\(j\)优于(或“相等“)\(k\),也就是费用更少。可以列出下面式子:

\[f[j]+a[i]*j+b[j] \leq f[k]+a[i]*k+b[k]\\ 即:a[i]*j \leq f[k]+a[i]*k+b[k]-f[j]-b[j]\\ a[i]*j-a[i]*k \leq f[k]+b[k]-f[j]-b[j]\\ (j-k)*a[i] \leq f[k]+b[k]-(f[j]+b[j])\\ a[i] \leq \frac{f[k]+b[k]-(f[j]+b[j])}{j-k}(注:因为j \neq k,所以可以移到右边)\\ 设g[j]=f[j]+b[j]\\ a[i] \leq \frac{g[k]-g[j]}{j-k}\\ -a[i] \geq \frac{g[j]-g[k]}{j-k} (注:这一步是为了让上下匹配)\\ \]

结论我们得知,如果出现了这种情况\(-a[i] \geq \frac{g[j]-g[k]}{j-k}\)\(j\)是优于(包括相等)\(k\)的,同理,如果是\(-a[i] \leq \frac{g[j]-g[k]}{j-k}\),则是\(k\)(包括相等)优于\(j\)。我们把\(j,k\)看成x坐标;\(g[j],g[k]\)看做y坐标,则代表决策点\(j\)的点的坐标为\((j,g[j])\)\(k\)\((k,g[k])\)。**划重点(引用上文的博客,有改动):此处移项需要遵循的原则是:参变分离。将只与\(i\)有关的视作未知量,用其他的量来表示出只与\(i\)有关的量。最后的公式尽量化成\(\frac{y_1-y_2}{x_1-x_2}\),而不是\(\frac{y_1-y_2}{x_2-x_1}\),对于这种情况我们可以两边\(*-1\),注意要变号 **!!!

维护凸包

那么我们使用这个式子,维护一个凸包(用不严谨的话来讲,给定二维平面上的点集,凸包就是将最外层的点连接起来构成的凸多边形,它能包含点集中所有的点——百度百科)。image-20200212172838426

设有三个点\(j1,j2,j3\),他们都是已经求出了值的。\(k1,k2\)代表斜率。很明显可以看出\(k2<k1\),我们设\(k0=-a[i]\)。有以下三种情况(为了应对以后不同大小的\(k0\),我们必须保证正确性,于是列出三种情况):

  1. \(k2<k1 \leq k0\),由上述结论知,\(j3优于j2优于j1\)
  2. \(k2 \leq k0 <k1\),由上述结论知,\(j3和j1优于j2\)
  3. \(k0 \leq k2 <k1\),由上述结论知,\(j1优于j2优于j3\)

综上,无论哪种情况\(j2\)这个决策点都不是最优的,也就是在后续的DP中,不论\(k0\)多大,\(j2\)永远不会被用于更新一个点,它是没用的,我们把它删去。image-20200212174229841

这个有什么用呢?维护凸包,说白了就是维护单调队列优化那样的队列。这个队列中存在一个最优决策点,就是最优的那个,队列中的每个点都有可能为后续的DP做贡献。我们在\(f[i]\)求完以后将它加入队列(如图的\(j3\)),并删除没用的点(如图的\(j2\))。在此贴上本题本部分代码

while(head<tail&&slope(q[tail-1],q[tail])<=slope(q[tail-1],i)) tail--;//slope函数是计算x,y两点的斜率;必须保证队列里至少有2个点
    q[++tail]=i;//q是队列,存的是点的编号

二分答案

我们求\(f[i]\)的时候,队列里必定存在着一个最优决策点,从它转移到\(i\)是最优的,通过以上的维护凸包,我们发现,对于这题我们维护的是下凸包(很形象,就是凸向下的),如果对于这题求花费最大,也就是把结论中的符号反过来维护的就是上凸包。不改变这题,相邻两个点间的斜率是递增的,如下图:image-20200214091405405

由于斜率是单调递增的,我们可以二分答案,那我们要二分找啥呢?

如上图:\(k1<k2<k3<k4<k5<k6<k7\),假设\(k0\)的大小是这样的:\(k1<k2<k3 \leq k0<k4<k5<k6\)

我们发现,\(k3 \leq k0\),由上述结论知,\(j4优于j3\)\(k0<k4\),由上述结论知,\(j4优于j5\)。其他以此类推。所以当前图中\(j4\)最优决策点,我们二分一条线段的右端点,找到第一个斜率\(>k0\)的,它的上一个节点(也就是那条线段的左端点)即是最优决策点。如此,可以用\(\log_2 n\)的时间求出最优决策点。需要注意的是二分这里有很多细节,我之前是二分左端点的,但是当所有的斜率都小于\(k0\)的时候,实际上最优的是最后一个点,但是我的程序二分到了倒数第二个端点,于是错了,卡了我好久,后开加了个特判过了。贴上代码(因为加了特判,写的很丑,见谅!同学们可以二分右端点):

l=head;
r=tail-1;
if(l==r){
	if(slope(q[l],q[l+1])<-a[i]) l++;//只有两个点时无法二分
}else{
	while(l<r){
		mid=(l+r-1)/2;
		if(slope(q[mid],q[mid+1])<-a[i]) r=mid;
		else l=mid+1;
	}
	if(tail>head)
		if(slope(q[l],q[l+1])>=-a[i]) l++;//特殊情况判断
}
//l即为最优决策点在队列中的下标
f[i]=f[q[l]]+(q[l]-i)*a[i]+b[q[l]];//用最优决策点更新f[i]

总结

实际上,斜率DP不是一个太难的东西,你可以把它想象成单调队列优化的工作模式,它的核心也是一个单调队列,但是它维护的规则与单调队列优化不一样,单调队列优化只是简单地基于数值维护,而斜率优化则是通过斜率维护,他们每加入一个新点时都要删去一些点。附上完整代码:

#include<cstdio>
int n,l,r,mid,head,tail,q[100005];
long long a[100005],b[100005],f[100005];
inline long double slope(int j,int k) {return (long double)((f[j]+b[j])-(f[k]+b[k]))/(long double)(j-k);}
int main(){
	freopen("t3.in","r",stdin);
	freopen("t3.out","w",stdout);
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
		scanf("%lld",&a[i]);
	for(int i=1;i<=n;i++)
		scanf("%lld",&b[i]);
	f[n]=0;
	head=tail=1;
	q[1]=n;
	for(int i=n-1;i>=1;i--){
		l=head;
		r=tail-1;
		if(l==r){
			if(slope(q[l],q[l+1])<-a[i]) l++;
		}else{
			while(l<r){
				mid=(l+r-1)/2;
				if(slope(q[mid],q[mid+1])<-a[i]) r=mid;
				else l=mid+1;
			}
			if(tail>head)
				if(slope(q[l],q[l+1])>=-a[i]) l++;
		}
		f[i]=f[q[l]]+(q[l]-i)*a[i]+b[q[l]];
		while(head<tail&&slope(q[tail-1],q[tail])<=slope(q[tail-1],i)) tail--;
		q[++tail]=i;
	}
	printf("%lld",f[1]);
	return 0;
}
posted @ 2020-02-14 09:54  MxCoder  阅读(...)  评论(...编辑  收藏