斜率优化dp

斜率优化

引言

当你看到这一篇博客时,相信你已经对基本的 d p dp dp模型有了了解,已经能独立推导出较为简单的 d p dp dp式子,但有时在进行 d p dp dp转移的过程,需要耗费较高的复杂度,朴素的 d p dp dp方程及转移只能得到部分分数,无法取得AC,此时就需要如斜率优化这样来减少时间复杂度的工具。当然,斜率优化也只是一种工具,必须在可以推出正确但效率较低的 d p dp dp式后才能发挥用处,故而 d p dp dp的基本功才是关键。

斜率优化的难度体现在:在 O I OI OI领域初步接触到与几何相关的应用,首次理解上稍困难,但在理解后,斜率优化的难度其实并不高,掌握套路就可以轻松运用。

基础斜率优化(双单调)

前文中提到,斜率优化是用来辅佐 d p dp dp的,但也并不能辅佐所有的 d p dp dp。能被斜率优化的 d p dp dp非常典型,以一道题目为例:打印文章

给出 N N N 个单词,每个单词有个非负权值 C i C_i Ci,现要将它们分成连续的若干段,每段的代价为此段单词的权值和的平方,还要加一个常数 M M M,即 ( ∑ C i ) 2 + M (\sum C_i)^2+M (Ci)2+M。现在想求出一种最优方案,使得总费用之和最小。

不难得出朴素 d p dp dp做法:
f i f_i fi为将前 i i i个单词划分的最小费用, s u m c i sumc_i sumci表示 C i C_i Ci的前缀和
f i = m i n j = 0 i − 1 ( f j + ( s u m c i − s u m c j ) 2 + M ) f_i=min_{j=0}^{i-1}(f_j+(sumc_i-sumc_j)^2+M) fi=minj=0i1(fj+(sumcisumcj)2+M)
(若无法理解朴素 d p dp dp,建议先不要学习斜率优化)

在朴素 d p dp dp的过程中,需要同时枚举 i , j i,j i,j,复杂度为 n 2 n^2 n2,显然无法通过此题。

若只枚举 i i i,我们只需要花较低的复杂度找到一个最优的 j j j,即可以使 ( f j + s u m c i 2 − 2 × s u m c i × s u m c j + s u m c j 2 + M ) (f_j+sumc_i^2-2\times sumc_i\times sumc_j+sumc_j^2+M) (fj+sumci22×sumci×sumcj+sumcj2+M)最小的 j j j,找到之后转移即可,斜率优化正是来帮助我们快速地寻找到 j j j

对于上式中的多项式,将其展开为多个单项式,即:
f i = m i n j = 0 i − 1 ( f j + s u m c i 2 − 2 × s u m c i × s u m c j + s u m c j 2 + M ) f_i=min_{j=0}^{i-1}(f_j+sumc_i^2-2\times sumc_i\times sumc_j+sumc_j^2+M) fi=minj=0i1(fj+sumci22×sumci×sumcj+sumcj2+M)

对于式子中的常数项,无论 j j j取何值,都需加上这一常数项,它对于寻找最优的 j j j没有作用,因此在斜率优化的过程中,我们暂时不需考虑常数项。

现在将 m i n min min删去,只考虑对于某一个处于范围 [ 0 , i ) [0,i) [0,i) j j j,则转移方程为:
f i = f j + s u m c i 2 − 2 × s u m c i × s u m c j + s u m c j 2 f_i=f_j+sumc_i^2-2\times sumc_i\times sumc_j+sumc_j^2 fi=fj+sumci22×sumci×sumcj+sumcj2

在这个方程中,存在四类单项式:
1.只与 i i i有关的,例如: s u m c i 2 , f i sumc_i^2,f_i sumci2,fi
2.只与 j j j有关的,例如: s u m c j 2 , f j sumc_j^2,f_j sumcj2,fj
3.与 i , j i,j i,j都有关的,例如: − 2 × s u m c i × s u m c j -2\times sumc_i\times sumc_j 2×sumci×sumcj

接下来进行移项
将只与 j j j有关的移向左侧,其余移向右侧
整理两侧式子为这样的形式:
f ( j ) = g ( i ) ∗ h ( j ) + r ( i ) f(j)=g(i)*h(j)+r(i) f(j)=g(i)h(j)+r(i)
f ( i ) f(i) f(i)即为只关于 i i i的多项式, g , h , r g,h,r g,h,r同理)

上式则可以整理为:
f j + s u m c j 2 = 2 × s u m c i × s u m c j + f i − s u m c i 2 f_j+sumc_j^2=2\times sumc_i\times sumc_j+f_i-sumc_i^2 fj+sumcj2=2×sumci×sumcj+fisumci2

初中我们学过一次函数的表达式 y = k x + b y=kx+b y=kx+b,与我们所得到的方程相似

f j + s u m c j 2 f_j+sumc_j^2 fj+sumcj2视为 y y y:因变量
2 × s u m c i 2\times sumc_i 2×sumci视为 k k k:斜率
s u m c j sumc_j sumcj视为 x x x:自变量
f i − s u m c i 2 f_i-sumc_i^2 fisumci2视为 b b b:截距

我们的最终目的是要找到最优的 j j j,更直接的目的,就是让 f i f_i fi尽可能的小,观察 f i f_i fi在哪里?

没错,在截距中!

由于 j j j的枚举范围是 [ 0 , i ) [0,i) [0,i),当我们计算 f i f_i fi时,显然对于 ∀ j ∈ [ 0 , i ) \forall j\in[0,i) j[0,i),自变量 s u m c j sumc_j sumcj与因变量 f j + s u m c j 2 f_j+sumc_j^2 fj+sumcj2均已知,已知 x , y x,y x,y,则可以将其视为平面直角坐标系的一个定点,故此时在平面直角坐标系中已经存在了 i i i个定点。

观察斜率,也是一个已知量,那么对于每一个 j j j,我们就可以得到一条斜率为 2 × s u m c i 2\times sumc_i 2×sumci,且过点 ( s u m c j , f j + s u m c j 2 ) (sumc_j,f_j+sumc_j^2) (sumcj,fj+sumcj2)的确定直线。直线确定,截距也就可以确定,进而 f i f_i fi也可以确定。

由于枚举值为 i i i,则斜率此时作为一个定值,也就是说,我们平移一条斜率为定值 2 × s u m c i 2\times sumc_i 2×sumci的直线,使其过某点 ( s u m c j , f j + s u m c j 2 ) (sumc_j,f_j+sumc_j^2) (sumcj,fj+sumcj2),就可以得到一个合法的 f i f_i fi

那怎么在合法的 f i f_i fi中找到最小的一个呢?

若对于每个点 ( s u m c j , f j + s u m c j 2 ) (sumc_j,f_j+sumc_j^2) (sumcj,fj+sumcj2),都计算出对应的 f i f_i fi为多少,再取比较出最小的,显然复杂度仍未 n 2 n^2 n2,起不到优化的作用。

有没有什么办法可以不考虑所有点,换言之,有没有一些点绝不可能作为答案?这样我们无需枚举这些点,以减少时间复杂度

当然是有的,我们仍以此题为例,在本题中, k ≥ 0 k\geq0 k0且单调递增, x ≥ 0 x\geq 0 x0且单调递增,假设在平面直角坐标系中存在三个点如图所示:
斜率优化dp_第1张图片
请试着拿一条斜率 ≥ 0 \geq 0 0的直线,观察这条直线过哪个点时,截距最小。

尝试后不难发现,当斜率 ∈ [ 0 , k A C ] \in[0,k_{AC}] [0,kAC]时, A A A为最优点;当斜率 ∈ ( k A C , + ∞ ) \in(k_{AC},+∞) (kAC,+)时, C C C为最优点;无论何时, B B B都不会为最优点,而 B B B则是刚才提到的绝不可能作为答案的点,可删除。

那么怎么筛选出形如 B B B的所有无用点呢?我们以一个更大的图为例来考虑这个问题:
斜率优化dp_第2张图片
在这个图中,再次进行尝试,不难看出有用点只有 I , J , H , G I,J,H,G I,J,H,G,其余均为无用点

将这四个点依次连接,如图所示:
斜率优化dp_第3张图片
它们构成了一个向外凸的形状,我们将其称之为凸包凸壳

由于它向外凸出,将一条斜率固定的直线从下向上平移,必然会与这个凸出的图形相切,无法到达凸包内部的点,故只有凸包上的点才为有效点

那么问题就转化为,如何找出这个凸壳?

已知 x x x单调递增,即图中的所有点加入的顺序应该是以 x x x从小到大的顺序排列的,顺序为:
I , A , B , D , E , J , C , F , H , G I,A,B,D,E,J,C,F,H,G I,A,B,D,E,J,C,F,H,G
我们来模拟一下凸包维护的过程

首先维护一个队列
I I I插入队列
A A A插入队列
判断插入 B B B点后, I , A , B I,A,B I,A,B三点能否构成凸包
由于 k I A > k A B k_{IA}>k_{AB} kIA>kAB,无法构成一个凸包,将 A A A从队列中弹出
B B B插入队列
判断插入 D D D点后, I , B , D I,B,D I,B,D三点能否构成凸包
由于 k I B > k B D k_{IB}>k_{BD} kIB>kBD,无法构成一个凸包,将 B B B从队列中弹出
D D D插入队列
判断插入 E E E点后, I , D , E I,D,E I,D,E三点能否构成凸包
由于 k I D < k D E k_{ID}kID<kDE,可以构成一个凸包
E E E插入队列
判断插入 J J J点后, I , D , E , J I,D,E,J I,D,E,J四点能否构成凸包,等价于判断 D , E , J D,E,J D,E,J三点能否构成凸包
由于 k D E > k E J k_{DE}>k_{EJ} kDE>kEJ,无法构成一个凸包,将 E E E从队列中弹出
判断 I , D , J I,D,J I,D,J三点能否构成凸包
由于 k I D > k D J k_{ID}>k_{DJ} kID>kDJ,无法构成一个凸包,将 D D D从队列中弹出
J J J插入队列
. . . ... ...

以这种方式:比较队列中 最后两点的斜率 与 队尾点和新点的斜率 大小,判断是否将队尾弹出,再将新点插入。

我们就可以将凸包成功维护出来。

有效点已经全部筛选出来了,接下来,就该去寻找最优点了。

可以很暴力的枚举每一个凸包上的点,取最小值作为答案,但仍存在更为优秀的一种做法:

仍以上图为例子:
k ∈ [ 0 , k I J ] k\in [0,k_{IJ]} k[0,kIJ]时,最优点为 I I I
k ∈ ( k I J , k J H ] k\in (k_{IJ},k_{JH]} k(kIJ,kJH]时,最优点为 J J J
k ∈ ( k J H , k H G ] k\in (k_{JH},k_{HG]} k(kJH,kHG]时,最优点为 H H H
k ∈ ( k H G , + ∞ ) k\in (k_{HG},+∞) k(kHG,+)时,最优点为 G G G
由于 k k k单调递增,
所以,若此时的最优点为 J J J,则以后任意时刻的最优点都不再可能是 I I I I I I尽管在凸包上,也沦为了无用点。
在队列中, I I I身居队首,于是可以很轻易地将队首弹出即可
在弹出所有无效的队首之后,此时队首的点,即为最优点!
而这个过程,实际上也就是单调队列优化的过程。

至此,我们通过一个双端(单调)队列,既维护了凸包,又找到了最优点

时间复杂度:
首先枚举 i i i需花费 O ( n ) O(n) O(n)
在此循环中,我们在时刻的维护队列,每个点都最多只会进出队列一次,故总复杂度由 O ( n 2 ) O(n^2) O(n2)降至 O ( n ) O(n) O(n)

给一份本题的标程:

#include
using namespace std;
const int N=5e5+10;
int n,q[N];
long long m,c[N],sc[N],f[N];
long long X(int p){return sc[p];}
long long Y(int p){return f[p]+sc[p]*sc[p];}
long long K(int p){return 2ll*sc[p];}
int main()
{
	while(~scanf("%d%lld",&n,&m))
	{
		for(int i=1;i<=n;i++) scanf("%lld",&c[i]),sc[i]=sc[i-1]+c[i];
		int head=1,tail=1;
		q[1]=0;
		for(int i=1;i<=n;i++)
		{
			while(head<tail&&K(i)*(X(q[head+1])-X(q[head]))>=Y(q[head+1])-Y(q[head])) head++;
			int j=q[head];
			f[i]=f[j]+(sc[i]-sc[j])*(sc[i]-sc[j])+m;
			while(head<tail&&(Y(i)-Y(q[tail]))*(X(q[tail])-X(q[tail-1]))<=(Y(q[tail])-Y(q[tail-1]))*(X(i)-X(q[tail]))) tail--;
			q[++tail]=i;
		}
//		memset(f,0x3f,sizeof(f));
//		f[0]=0;
//		for(int i=1;i<=n;i++) for(int j=0;j
		printf("%lld\n",f[n]);	
	}
	return 0;
}

注意事项:
1.在计算斜率时,由于需要使用除法,存在精度问题,所以通过去分母,将式子转为乘法形式比较大小。
2.为了防止一些莫名其妙的麻烦,在比较斜率大小时需要写成 > = >= >= < = <= <=
3.弹队首队尾时都为 w h i l e while while循环
4.注意括号,不要括错!不要漏括号!

例题:
斜率优化dp_第4张图片

双单调斜率优化模型讨论

在这类题目模型中,有时并不是需要维护单调队列,而是需要维护单调栈
例如:柠檬
那么在什么时候需要维护单调队列,什么时候又需要维护单调栈呢?
下面将对这个问题进行讨论:

首先让我们来考虑一下凸包的形状,在上文例题中,凸包是向右下凸出的,思考如果我们不限制 k ≥ 0 k\geq 0 k0,那么凸包将是什么形状?

答案是这样:
斜率优化dp_第5张图片
凸包整体向下凸起

那有没有向上凸起的凸包呢?
当然有!
如果我们将斜率优化题目中所求改为求截距的最大值,那么凸包就会变成:
斜率优化dp_第6张图片
故影响凸包形状的,实际上是题目中所求的是最小值还是最大值。

接下来判断如何确定使用单调队列还是单调栈。
影响因素有三个:
1.凸包的形状
2. k k k的单调性
3. x x x的单调性

举一个例子:
k , x k,x k,x均单调递增,凸包下凸
即:
斜率优化dp_第7张图片
随着 k k k的增大,最优决策点的移动方向是向右的,即向 x x x增大的方向,于此同时 x x x也在增大,向右移动,二者同向
所以需要维护一个队列,队首移动 k k k,队尾移动 x x x,同时向右移动
同理,当 k , x k,x k,x均单调递减,凸包下凸时,也需要维护单调队列

k k k单调递增, x x x单调递减呢?
此时二者移动方向相反,新插入的点实际上斜率更小,用栈维护,才能维护出斜率单调递增
同理可得 k k k单调递减, x x x单调递增时,也需要用单调栈

对于上凸的情况请读者自行讨论,这里只给出结论:

当凸包下凸时, k , x k,x k,x单调性相同,使用单调队列, k , x k,x k,x单调性不同,使用单调栈;
当凸包上凸时, k , x k,x k,x单调性相同,使用单调栈, k , x k,x k,x单调性不同,使用单调队列。

k不单调,x单调的斜率优化

承接上文中,当我们维护出凸包后,我们可以枚举凸包上的每一个点,求最小值即为答案。
对此我们加以优化,用单调队列将复杂度降得更低,但此优化的前提是 k k k单调递增
那如果 k k k并不单调递增,我们只好另寻他路
实际上很简单:二分!
由于凸包中相邻两点的斜率具有单调性,所以可以二分找出最优点,以优化时间复杂度,将复杂度降至 n l o g n nlogn nlogn
例题:任务安排3

x不单调或x,k均不单调的斜率优化

由于插入点不再可以用队列这样的线性数据结构去维护,所以难度大大提高
常见的维护方法有: C D Q CDQ CDQ分治,平衡树,李超线段树

C D Q CDQ CDQ分治:
由于 x , k x,k x,k均不单调,而我们的斜率优化需要让他们单调才可以进行,在 d p dp dp的过程中只能将 [ 0 , i ) [0,i) [0,i)的转移到 i i i,在编号上又存在着一种单调性关系,同时需要维护三种单调性,很自然的可以想到解决三维偏序问题的 C D Q CDQ CDQ分治。

1.将一个点所对应的 i d , k , x id,k,x id,k,x都提前预处理,存入结构体中
2.将结构体按照 k k k值排序,处理第一维偏序(斜率)
3.进行 C D Q CDQ CDQ分治,每次将区间划分为两部分,划分的方式采用将编号较小的一半放在左侧,编号较大的一半放在右侧,维护第二维偏序(编号)
4. C D Q CDQ CDQ分治递归左区间
5.将左侧按照 x x x值排序,使左侧单调,此时由于右侧均未被递归, k k k值仍有序,构成了左侧 x x x值单调,右侧 k k k值单调,左侧的编号大小均小于右侧的编号大小
6.维护出左侧的所有点构成的凸包
7.用左侧维护出的凸包更新右侧的答案,由于右侧斜率单调,做法如同双单调型即可
8. C D Q CDQ CDQ分治递归右区间

例题:
在这里插入图片描述

你可能感兴趣的:(基础硬件,算法,动态规划,图论)