之前研究过一段时间的斜率优化,总觉的什么时候该写写。
# 普通斜优
斜率优化是一种优化 DP 的方法,主要应用于 1D/1D 的转移中(也有反例),一般可以把复杂度优化成 O(n) 或 O(nlogn)。斜率优化主要利用了单调性,所以在阅读下文之前,请至少熟练掌握单调队列。
本文以下面这道题为例讲解斜率优化,并对涉及到的一些本题之外的问题,在后文提供练习题。
[HNOI2008]玩具装箱
# 题目描述
有 n 个玩具,第 i 个长度为 ci。将第 i 到第 j 个玩具装到一个箱子里会花费 (j−i+k=i∑jck−L)2 的代价。求总代价最小值。
1⩽n⩽5×104,1⩽L,ci⩽107
# 输入格式
第一行两个整数,n 和 L。
第 2 到第 (n+1) 行,第 (i+1) 行一个整数 ci。
# 输出格式
一行一个整数代表最小总代价。
# 样例
样例输入样例输出# 思路
设计 DP。
设 fi 表示第 i 个玩具作为其所处的箱子的结尾的时候,前面所有箱子的总代价的最小值。记 si 为 ci 的前缀和,得到 n2 状态转移方程:fi=1⩽j⩽imin(fj+(i−j+si−sj−L−1)2)。记 wi 为 si+i,得:fi=1⩽j⩽imin(fj+(wi−wj−L−1)2)
至于为什么后面要 −1?是因为实质上是第 (j+1) 个到第 i 个玩具装到一个箱子中所以是 i−(j+1)=i−j−1 应该只有我一开始不知道吧。
现在这个转移是 n2 的,显然会炸,考虑优化。
# 思考方式
下面将以两种思维解释斜优的过程:数形结合和线性规划。
先变化一下转移,变化的方法在后面给出,这样变化的好处也会体现。假设现在有两个决策点 j,k(j>k),而从 j 转移到 i 比从 k 转移到 i 优,即:
fj+(wi−wj−L−1)2fj+(wi−L−1)2−2(wi−L−1)wj+wj2fj−2(wi−L−1)wj+wj2fj−fk+wj2−wk2⩽fk+(wi−wk−L−1)2⩽fk+(wi−L−1)2−2(wi−L−1)wk+wk2⩽fk−2(wi−L−1)wk+wk2⩽2(wi−L−1)(wj−wk)
由于 j>k 且 wi 递增,所以 wj−wk>0(可能会遇到等于 0 的情况,需要特判为 INF),即:
wj−wkfj+wj2−(fk+wk2)⩽2(wi−L−1)
此处变化的原则是把仅关于 i 的项和仅关于 j 的项放在一边,把关于 i 且关于 j 的项放在另一边,至于到底在那边并不重要,换边乘 −1 就行了。
变化完之后,我们发现如果记 yi=fi+wi2,xi=wi,那么式子左边就是 xj−xkyj−yk,形如一个直线的斜率。也就是说,如果把决策点按照 (wi,fi+wi2) 为坐标的样子标记在平面直角坐标系上,那么如果决策点 p1 和决策点 p2 (xp1>xp2) 的连线的斜率 ⩽2(wi−L−1) 那么决策点 p1 优于决策点 p2。
对于刚学斜优的同学,为避免麻烦的细节处理,可以把斜优模板化。比如此处建议尽量把最终的式子化为 xj−xkyj−yk 的形式,而不是 xj−xkyk−yj。熟练掌握之后倒也无所谓
进一步的,我们假设有三个候选决策点 p1,p2,p3,他们在平面上形如这样:
![]()
他们满足条件:k1>k2。记 2(wi−L−1) 为 k0,那么接下来 k1,k2 与 k0 的关系有以下几种:
- k0<k2<k1,此时 p1 优于 p2 优于 p3。
- k2⩽k0<k1,此时 p1 优于 p2 且 p3 优于 p2。
- k2<k1⩽k0,此时 p3 优于 p2 优于 p1。
综上,在 k1>k2 的条件下,无论如何决策点 p2 一定是不优的,于是可以把从候选决策点中删除,像这样:
![]()
再把三个点扩展到平面上的很多点,对于任意三个点进行上述操作。在本题中,剩下的点从左到右依次连线形成的直线的斜率单调递增。这样连线画图的结果就像一个下凸包。
所有内角大小都在 [0,π] 范围内的简单多边形叫凸多边形。
在平面上能包含所有给定点(即具有凸性)的最小凸多边形叫做凸包。
下凸包就是凸包的下半部分。如图是维护的下凸包的例子:
![]()
在寻找决策点的过程中,我们一定能找到三个决策点 p1,p2,p3 满足 kp1,p2⩽k0<kp2,p3。此时 p2 优于 p1 且优于 p3,基于本题斜率单调递增的性质,其他直线要么 kp<k0 要么 kp>k0,他们相应的决策点均不优于 p2,那么我们称 p2 为最优决策点。
在已经维护好的下凸包上,从前往后依次找到第一个斜率大于 k0 的线段,它的左端点就是最优决策点。由于维护好的凸包从左往右斜率单调,所以可以二分找到最优决策点。
忽略 min,把上面的式子变化成 y=kx+b 的形式,即:
fi=fj+((wi−L−1)−wj)2=fj+(wi−L−1)2−2(wi−L−1)wj+wj2fj+wj2=2(wi−L−1)wj+fi−(wi−L−1)2
此处变化的原则是:
- 把 gi∗gj 看成 kx。
- fi 必须在 b。
- y 只含 gj,并按需与 kx 合并。
- 如果 kx 单减,建议两边同乘 −1 使其单增。
对于这种划分:
- y 为 fj+wj2
- k 为 2(wi−L−1)
- x 为 wj
- b 为 fi−(wi−L−1)2
假如有一堆点在平面上长这样:
![]()
我们的目的是使 fi 最小,即使 b=fi−(wi−L−1)2 最小,就是要求一个过一点的斜率为 2(wi−L−1) 直线使其截距最小,转为线性规划问题。
那么最优决策点就很好找了:
![]()
图中 F 即为最优决策点,且最优决策点必然处于凸包上。
即便有两种思路,但是建议结合使用:
- 在思考前,主要利用代数法转换方程,确定斜率形式。
- 在思考阶段建议多用线性规划思想,利用图形直观感受,更好得判断单调性(后文会讲)。
# 具体实现
用单调队列 q 维护凸包,开头为 s,结尾为 t,操作如下:
- 在凸包上找到最优决策点 j。
- 用 j 更新 i。
- 把 i 作为下一个决策点加入平面,更新凸包
对于操作 3,比较 qt−1、qt 和 i,看是否能删掉 qt:能删就删,重复操作 3;不能删就把 i 加入队列。
判断能否删点有两种方法(可以结合上文 “数形结合” 中的图理解):
slope(q[t],q[t-1])>slope(q[t],i)
slope(q[t],q[t-1])>slope(q[t-1],i)
两者是等价的,可把 q[t]
删除,读者自己思考。
对于操作 1,暴力找的话复杂度还是 O(n2),二分查找可使复杂度降为 O(nlogn)。
复杂度瓶颈在于操作 1。
# 再优化
如果要继续降低复杂度的话,将要利用决策单调性,其简单定义为:
对于形如 fi=fj+W(i,j) 的方程,即 1D/1D 转移方程,设 ti 表示 fi 的最优决策点。那么决策单调性可表示为 ∀j⩽k,tj⩽tk。也就是说 ti 不降。
证明见下。
# 四边形不等式
设二元函数 W(x,y) 定义域为整数。若 ∀a⩽b⩽c⩽d,W(a,c)+W(b,d)⩽W(a,d)+W(b,c),那么函数 W(x,y) 满足四边形不等式。
从形的角度来看,把 W(x,y) 画成这样:
![]()
那么满足左下角加右上角小于等于左上角加右下角的 W(x,y) 满足四边形不等式。
四边形不等式也可表示为 ∀a<b,W(a,b)+W(a+1,b+1)⩽W(a+1,b)+W(a,b+1),证明如下:
∵∀a<c,W(a,c)+W(a+1,c+1)⩽W(a+1,c)+W(a,c+1)
(1)
∴∀a+1<c,W(a+1,c)+W(a+2,c+1)⩽W(a+2,c)+W(a+1,c+1)
(2)
(1)、(2) 式相加:W(a,c)+W(a+2,c+1)⩽W(a+2,c)+W(a,c+1)
进一步地:∴∀a⩽b⩽c,W(a,c)+W(b,c+1)⩽W(b,c)+W(a,c+1)
同理:∴∀a⩽b⩽c⩽d,W(a,c)+W(b,d)⩽W(b,c)+W(a,d)
证毕。
# 决策单调性
对于一个 1D/1D 的转移方程 fi=fj+W(i,j),函数 w 满足四边形不等式是其满足决策单调性的充分条件证明如下:
∵ti 在 fi 的决策点中最优
∴∀i∈[1,n],j∈[0,ti),fti+W(ti,i)⩽fj+W(j,i)
(3)
又 ∵∀k∈(i,n],j<ti<i<k,且函数 w 满足四边形不等式
∴W(j,i)+W(ti,k)⩽W(j,k)+W(ti,i)
∴W(ti,k)−W(ti,i)⩽W(j,k)−W(j,i)
与 (3) 式相加得:fti+W(ti,k)⩽fj+W(j,k)
也就是说,从任意在 i 的最优决策点 ti 之前的决策点,转移到 i 后面任意一个决策点都没有从 ti 转移优。那么对于 i 之后的决策点 k,他的最优决策点一定属于 [ti,k),那么 ti 不减,决策具有单调性。
证毕。
# 证明决策单调性
提供三种方法,后两种与前面的思考方式有关。
# 四边形不等式法
本题转移方程形如 fi=fj+W(i,j),其中 W(i,j)=(wi−wj−L−1)2,证明如下:
记 Q=wi−wj−L−1
∴W(i,j)=Q2
∴W(i+1,j+1)=(wi+1−wj+1−L−1)2=((si+1+i+1)−(sj+1+j+1)−L−1)2=((si+ci+1+i+1)−(sj+cj+1+j+1)−L−1)2=((si+i)−(sj+j)−L−1+ci+1−cj+1)2=((wi−wj−L−1)+ci+1−cj+1)2=(Q+ci+1−cj+1)2,W(i,j+1)=(wi−wj+1−L−1)2=(wi−(sj+1+j+1)−L−1)2=(wi−(sj+cj+1+j+1)−L−1)2=(wi−(sj+j)−L−1−cj+1−1)2=((wi−wj−L−1)−cj+1−1)2=(Q−cj+1−1)2,W(i+1,j)=(wi+1−wj−L−1)2=((si+1+i+1)−wj−L−1)2=((si+ci+1+i+1)−wj−L−1)2=((si+i)−wj−L−1+ci+1+1)2=((wi−wj−L−1)+ci+1+1)2=(Q+ci+1+1)2
∴W(i,j)+W(i+1,j+1)=2Q2+2Qci+1−2Qcj+1+ci+12−2ci+1cj+1+cj+12,W(i+1,j)+W(i,j+1)=2Q2+2Qci+1+ci+12+2ci+1+2−2Qcj+1+cj+12+2cj+1
∴W(i,j)+W(i+1,j+1)−W(i+1,j)−W(i,j+1)=−2−2ci+1−2cj+1−2ci+1cj+1=−2(ci+1+1)(cj+1+1)
又 ∵ci,cj≥1
∴−2(ci+1+1)(cj+1+1)⩽−8<0
∴W(i,j)+W(i+1,j+1)⩽W(i+1,j)+W(i,j+1)
四边形不等式成立,所以方程具有决策单调性。
证毕。
# 数形法
由于 k0=2(wi−L−1) 单增,所以第一个斜率大于 k0 的线段逐渐往后移,最优决策点后移,即最优决策点的位置单增。
证毕。
# 线性规划法
由于直线斜率 2(wi−L−1) 单增,故其与凸包的切点横坐标单增,最优决策点的位置单增。
证毕。
# 再实现
对于上面的实现方法,我们只用更改操作 1:在第一根线段的斜率小于等于 k0 时弹出队首,重复直至第一根线段的斜率大于 k0,此时队首即为最优决策点。
根据决策单调性,后面决策点的最优决策点一定在队首后面,且每个决策点至多进出队列 1 次,时间复杂度 O(n)。
# 本题代码
基于上面的方法,建议自己实现一遍,再看我的代码。
玩具装箱-直观理解题目链接 | #include <bits/stdc++.h> |
| #define db double |
| using namespace std; |
| const int N=50005; |
| int n,L; |
| db c[N],s[N],f[N]; |
| int head,tail,Q[N]; |
| |
| inline db w(int i){return s[i]+i;} |
| inline db X(int i){return w(i);} |
| inline db Y(int i){return f[i]+w(i)*w(i);} |
| inline db slope(int i,int j){return (Y(i)-Y(j))/(X(i)-X(j));} |
| |
| int main(){ |
| scanf("%d%d",&n,&L); |
| for(int i=1;i<=n;i++) scanf("%lf",&c[i]); |
| for(int i=1;i<=n;i++) s[i]=s[i-1]+c[i]; |
| |
| head=tail=1; |
| |
| for(int i=1;i<=n;i++){ |
| while(head<tail&&slope(Q[head],Q[head+1])<=2*(w(i)-L-1)) ++head; |
| |
| f[i]=f[Q[head]]+(w(i)-w(Q[head])-L-1)*(w(i)-w(Q[head])-L-1); |
| |
| while(head<tail&&slope(i,Q[tail-1])<=slope(Q[tail-1],Q[tail])) --tail; |
| |
| Q[++tail]=i; |
| |
| } |
| |
| printf("%lld\n",(long long)f[n]); |
| return 0; |
| } |
但是一般不建议这样写,除法容易丢精度,所以我们把斜率的除换成交叉相乘:
玩具装箱-建议写法题目链接 | #include <bits/stdc++.h> |
| #define db double |
| using namespace std; |
| const int N=50005; |
| int n,L; |
| db c[N],s[N],f[N]; |
| int head,tail,Q[N]; |
| inline db w(int i){return s[i]+i;} |
| inline db X(int i){return w(i);} |
| inline db Y(int i){return f[i]+w(i)*w(i);} |
| inline int chk1(int i,int j,int p){return Y(j)-Y(i)<=2ll*(w(p)-L-1)*(X(j)-X(i));} |
| inline int chk2(int i,int j,int p){return (Y(p)-Y(i))*(X(j)-X(i))<=(Y(j)-Y(i))*(X(p)-X(i));} |
| |
| int main(){ |
| scanf("%d%d",&n,&L); |
| for(int i=1;i<=n;i++) scanf("%lf",&c[i]); |
| for(int i=1;i<=n;i++) s[i]=s[i-1]+c[i]; |
| head=tail=1; |
| for(int i=1;i<=n;i++){ |
| while(head<tail&&chk1(Q[head],Q[head+1],i)) ++head; |
| f[i]=f[Q[head]]+(w(i)-w(Q[head])-L-1)*(w(i)-w(Q[head])-L-1); |
| while(head<tail&&chk2(Q[tail-1],Q[tail],i)) --tail; |
| Q[++tail]=i; |
| } |
| printf("%lld\n",(long long)f[n]); |
| return 0; |
| } |
# 注意事项
斜率优化代码一般很短,但是细节较多,简单列举一下:
- 尽量不计算斜率,用交叉相乘,防止丢精。
- 判断是否需要在队列里插入初始值,如例题。
- 一般来说,可以出队的边界情况是
head<tail
。 - 如 xi 可能出现相等的情况:如果你使用的是交叉相乘,那你不用额外做什么;如果你计算了斜率,那就要特判一下
x[i]==x[j]?INF:...
。 - 一定要根据决策的优劣、题目要求的 min/max 来判断要维护上凸还是下凸包。
- 交叉相乘的时候,尽量把乘出去的
x[j]-x[i]
弄成正数,这样就不用考虑变号的问题了。 - 建议比较都用
<=
或 >=
代替 <
或 >
,因为斜率相等的点取那个都无所谓,这样还可以去重。 - 注意看交叉相乘会不会炸
long long
,会的话换 double
,输出注意转整型。 - 一定要计算斜率的话要用
long double
。 - 注意 xi 及斜率的单调性,不满足单调的要使用其他方法,见下文。
# 不单调斜优
不单调,即斜优的某些元素不单调,如下:
# 斜率不单调
亦可解释为最优决策点不单调,就是不能证明决策单调性,此时不能再弹出队首,只能二分。
# 横坐标不单调
此时插入决策点时可能会插在凸包中间,而队列不支持这种操作。
可以使用平衡树实现插入、查找前驱操作,或着用 CDQ 分治提供单调性,具体见下。
也可使用李超线段树,但是我不会(
# 斜率、横坐标均不单调
用平衡树。把 k0 放到平衡树上找前驱,再更新 dp 值,最后 insert
。
用 CDQ,步骤如下:
- 把 xi 小于 mid 的点作为左区间,其余作为右区间(如果 xi 单调则原区间的左区间就是左区间,右区间就是右区间)。
- 递归左区间。
- 对于左区间用单调队列维护出凸包(正确性见下),用它更新右区间。
- 递归右区间。
- 最后对整个区间对 xi 排序,此时这个区间 xi 单调,上一级区间可以直接对于本区间维护凸包,步骤 3 正确性得证。
# 练习
# 普通斜优
[ZJOI2007] 仓库建设
[APIO2014] 序列分割
[APIO2010] 特别行动队
[CEOI2004] 锯木厂选址
[NOIP2018 普及组] 摆渡车
[USACO08MAR] Land Acquisition G
[SDOI2016] 征途
[JSOI2009] 火星藏宝图
丝之割
[COCI2018-2019#4] Akvizna
[NOI2016] 国王饮水记
# 不单调斜优
任务安排 & [SDOI2012] 任务安排
[NOI2007] 货币兑换
[CEOI2017] Building Bridges
[SDOI2012] 基站建设
[NOI2014] 购票
# 参考资料
- ChatGPT
- 辰星凌的博客
- OI-wiki
- 其他大佬的博客