• 斜率优化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且单调递增,假设在平面直角坐标系中存在三个点如图所示:
    在这里插入图片描述
    请试着拿一条斜率 ≥ 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的所有无用点呢?我们以一个更大的图为例来考虑这个问题:
    在这里插入图片描述
    在这个图中,再次进行尝试,不难看出有用点只有 I , J , H , G I,J,H,G I,J,H,G,其余均为无用点

    将这四个点依次连接,如图所示:
    在这里插入图片描述
    它们构成了一个向外凸的形状,我们将其称之为凸包凸壳

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

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

    已知 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
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

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

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

    双单调斜率优化模型讨论

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

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

    答案是这样:
    在这里插入图片描述
    凸包整体向下凸起

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

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

    举一个例子:
    k , x k,x k,x均单调递增,凸包下凸
    即:
    在这里插入图片描述
    随着 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分治递归右区间

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

  • 相关阅读:
    在CentOS 7上安装JDK 17
    -求e的值-
    EFK部署centos7.9(一)ES单节点部署
    云原生入门 第五章:kubernetes学习实践
    LeetCode:771.宝石与石头
    21. 合并两个有序链表 --力扣 --JAVA
    PyQt5快速开发与实战 8.1 窗口风格
    Linux入门教程||Linux 用户和用户组管理
    万字总结随机森林原理、核心参数以及调优思路
    swagger---接口文档管理生成管理工具
  • 原文地址:https://blog.csdn.net/weixin_55355427/article/details/126298299