zoukankan      html  css  js  c++  java
  • 【易懂】斜率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 eq 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;
    }
    
  • 相关阅读:
    解决 ThinkPHP5 RCE 在PHP7下,不能使用包含的问题
    文件上传 之 条件竞争
    ThinkPHP3.2.4 order方法注入
    Mysql 不能使用逗号的情况
    MSSQL 注入笔记
    ThinkPHP 5.0.15中的update注入漏洞
    Thinkphp5 由Request导致的RCE漏洞版本小结
    即学即用,轻松搞定这些选择器!(上)
    JavaScript的使用你知道几种?(上)
    前端修炼の道 | <div> 标签简介
  • 原文地址:https://www.cnblogs.com/cjz-IT/p/easy_slope_DP.html
Copyright © 2011-2022 走看看