斜率优化 DP
顾名思义,运用数形结合的思想,将 DP 中求解的 最优值 问题 转化为 坐标系中相切问题,根据直线的斜率来求解
一般来说,斜率优化 DP 有几个前提:
- 朴素 DP 转移时需要枚举 决策,可以将决策的 O ( n ) O(n) O(n)枚举 优化到 O ( l o g ) O(log) O(log) 甚至 O ( 1 ) O(1) O(1),而 不可以在状态上进行优化
- 将外层的枚举变量当成常量后,转移方程可以写成 阶段 i i i 与 决策 j j j 的 一次函数,即:不包含其他未知量,不包含 非线性关系
通过具体题目来看
例 1 引入
一般来说,斜率优化DP 的题目会比较复杂,给的信息比较多,需要抽象出 准确的转移式
设 f [ i ] f[i] f[i] 表示完成 前 i i i 个任务的最小时间, T [ i ] T[i] T[i] 表示 时间的前缀和 , C [ i ] C[i] C[i] 表示费用前缀和
由任务是一段一段进行的,我们容易想到 对于当前 i i i,枚举以 i i i 为结尾一段任务的开头,从而转移
但本题有个不太好处理的点,机器启动的时间 S S S ,它会对后面的所有任务产生影响
运用一个技巧:费用提前计算
在当前 阶段 i i i 时,提前计算本次启动 对 后面所有任务费用的增量,直接加到 f [ i ] f[i] f[i] 里
转移有: f [ i ] = min 0 ≤ j < i ( f [ j ] + ( C [ i ] − C [ j ] ) × T [ i ] + S × ( C [ n ] − C [ j ] ) ) f[i]=\min_{0\leq j<i} (\ f[j]\ + \ (C[i]-C[j])\times T[i]\ +\ S\ \times\ (C[n]-C[j]) ) f[i]=0≤j<imin( f[j] + (C[i]−C[j])×T[i] + S × (C[n]−C[j]))
这就是 斜率优化 DP 的第一步,写成朴素的 DP 转移式,标明决策上下界
第二步,我们对式子进行变形,去掉 m i n min min ,分组、移项 f [ j ] − S ⋅ C [ j ] = T [ i ] ⋅ C [ j ] ‾ + f [ i ] − C [ i ] ⋅ T [ i ] − S ⋅ C [ n ] f[j]- S\cdot C[j] = \underline{ T[i]\cdot C[j]} +f[i]-C[i]\cdot T[i]-S\cdot C[n] f[j]−S⋅C[j]=T[i]⋅C[j]+f[i]−C[i]⋅T[i]−S⋅C[n]
注意到,式子的左边只与 j j j 有关,式子右边下划线是 i i i 与 j j j (有关) 的乘积,剩下则是 要求的 f [ i ] f[i] f[i] 和 一些常数
如果令左侧为 Y ( j ) Y(j) Y(j) ,右侧 T [ i ] T[i] T[i] 为 斜率 k k k , C [ j ] C[j] C[j] 为 X ( j ) X(j) X(j)
就能得到: Y ( j ) = k ⋅ X ( j ) + b Y(j)=k\cdot X(j)+b Y(j)=k⋅X(j)+b ,这就化成了 关于 j j j 的一次函数
回到问题,求 min f [ j ] \min f[j] minf[j] ?等价于求 b b b 最小,也就是 截距 最小
我们上图:
若干个黑色的 “决策” 构成了一条折线,对于当前 斜率确定 的直线,我们考虑找到使得 截距 最大的那个决策点
显然,它正是 直线 与 折线 相切的位置
回忆单调队列优化 DP 的一个核心思想:排除无用决策,维护决策集合的高效和有序
这里我们也可以借用,显然由一些点是永远用不着的
最终,感性理解 + 理性证明,我们得出结论:只需要维护一条下凸折线
而下凸折线有着一条优秀的性质:斜率单调递增。
至于切点,我们可以发现就是 第一个斜率大于 k k k 的位置。
这个是可以二分的。但本题还由一种更优秀的做法,发现枚举 i i i 的过程中,直线的斜率 T [ i ] T[i] T[i] 同样单调递增
那么只需实时维护队头元素即可,斜率小于当前 k k k 时直接出队,队头就是最优决策
下面来说 如何维护下凸折线
发现若当前新加入的点与队尾的连线斜率小了,可以把队头出队,直到斜率单调
具体实现来说,队列里放决策就行了, X ( j ) X(j) X(j), Y ( j ) Y(j) Y(j),或者斜率都可以现算
注意开 long long
代码:( 灰常臃肿 )
#include<bits/stdc++.h>
using namespace std ;
const int N = 51000 , inf = 1e9 ;
typedef long long LL ;
// 费用提前计算
int n ;
LL f[N] , T[N] , C[N] , S ;
deque<int> q ;
int main()
{
scanf("%d%lld" , &n , &S ) ;
for(int i = 1 ; i <= n ; i ++ ) {
scanf("%lld%lld" , &T[i] , &C[i] ) ;
T[i] += T[i-1] ; C[i] += C[i-1] ;
}
q.push_front( 0 ) ;
for(int i = 1 ; i <= n ; i ++ ) {
while( q.size() > 1 && f[q[1]]-f[q[0]] <= (C[q[1]]-C[q[0]])*(T[i]+S) ) q.pop_front() ;// 维护队头
f[i] = f[q.front()] + (C[i]-C[q.front()])*(T[i]) + S*(C[n]-C[q.front()]) ;
while( q.size() > 1 && (f[q[q.size()-1]]-f[q[q.size()-2]])*(C[i]-C[q[q.size()-1]]) >= (f[i]-f[q[q.size()-1]])*(C[q[q.size()-1]]-C[q[q.size()-2]]) ) q.pop_back() ;
q.push_back( i ) ;
}
printf("%lld" , f[n] ) ;
return 0 ;
}
其实本题能这么 斜率优化 还是有一些性质的:
- 观察 X ( j ) = C [ j ] X(j)=C[j] X(j)=C[j] ,同样单增,这是斜率优化的 必要条件,即新加入决策点的横坐标务必单增,否则可能需要一些横纵坐标互换,转化坐标系的技巧或者高级数据结构,简单说就是暂时搞不了
- 所求直线的斜率单调递增,这使得我们可以实时维护队头来确保最优决策,但是当斜率不单增时,就不得不使用二分了
例 2 二分
本题由于 T [ i ] T[i] T[i] 不再单增,需要二分。
细节很多
#include<bits/stdc++.h>
using namespace std ;
const int N = 3e5 + 100 ;
typedef long long LL ;
int n ;
LL s , T[N] , C[N] , f[N] ;
deque<int> q ;
int main()
{
scanf("%d%lld" , &n , &s ) ;
for(int i = 1 ; i <= n ; i ++ ) {
scanf("%lld%lld" , &T[i] , &C[i] ) ;
T[i] += T[i-1] , C[i] += C[i-1] ;
}
q.push_back( 0 ) ;
for(int i = 1 ; i <= n ; i ++ ) {
LL nowK = T[i]+s ;
// 二分查第一个大于当前斜率的决策
int l = 0 , r = q.size()-1 , mid ;
while( l+1 < r ) {
mid = ( l + r ) >> 1 ;
if( f[q[mid+1]]-f[q[mid]] < nowK*(C[q[mid+1]]-C[q[mid]]) ) l = mid ;
else r = mid ;
}
int j ;
if( f[q[l+1]]-f[q[l]] > nowK*(C[q[l+1]]-C[q[l]]) ) j = l ;
else j = r ;
// 更新
f[i] = f[q[j]] + s*(C[n]-C[q[j]]) + T[i]*(C[i]-C[q[j]]) ;
while( q.size() > 1 && (f[i]-f[q[q.size()-1]])*(C[q[q.size()-1]]-C[q[q.size()-2]]) <= (f[q[q.size()-1]]-f[q[q.size()-2]])*(C[i]-C[q[q.size()-1]]) ) q.pop_back() ;
q.push_back( i ) ;
}
printf("%lld\n" , f[n] ) ;
return 0 ;
}
例 3 决策带限制
同样是 “一段一段” 的 DP,唯一需要注意的是决策上界
类似单队的技巧,决策合法时再加入队列
#include<bits/stdc++.h>
using namespace std ;
#define Y(y) (f[y]-S[y]+a[y+1]*(y))
#define X(x) (a[x+1])
#define tt (q.size()-1)
typedef long long LL ;
const int N = 5e5 + 100 ;
const LL inf = 0x3f3f3f3f3f3f3f3f ;
int T ;
int n , K ;
LL f[N] , S[N] , a[N] ;
deque<int> q ;
int main()
{
scanf("%d" , &T ) ;
while( T -- ) {
scanf("%d%d" , &n , &K ) ;
for(int i = 1 ; i <= n ; i ++ ) {
scanf("%lld" , &a[i] ) ;
S[i] = S[i-1] + a[i] ;
}
q.clear() ;
q.push_back( 0 ) ;
memset( f , 0x3f , sizeof f ) ;// 要么入队前判,要么初值赋小点,防爆
// for(int i = 1 ; i <= n ; i ++ ) f[i] = 1e12 ;
f[0] = 0 ;
for(LL i = K ; i <= n ; i ++ ) {
while( q.size()>1 && (Y(q[1])-Y(q[0])) <= (X(q[1])-X(q[0]))*i ) {
q.pop_front() ;
}
int j = q.front() ;
f[i] = f[j] + S[i]-S[j] - a[j+1]*(i-j) ;
if( f[i-K+1] < inf ) { // 十分关键
while( q.size()>1 && ( ((Y(q[tt])-Y(q[tt-1]))*(X(i-K+1)-X(q[tt])) >= (Y(i-K+1)-Y(q[tt]))*(X(q[tt])-X(q[tt-1]))) ) ) {
q.pop_back() ;
}
q.push_back( i-K+1 ) ;
}
}
printf("%lld\n" , f[n] ) ;
}
return 0 ;
}
我们发现,本题的横坐标是 不严格单增 的,对于横坐标相同的决策可能需要特判?
—— 本题不需要,横坐标相同需要取纵坐标最大,而 Y Y Y 值单增,后来的会把前面的顶掉
但其他题可能要特判!!
例 4 性质 + 特性
分段式 dp ,加上题目中 平方的样子,联想斜率优化
比较套路的考虑方式: f [ i ] f[i] f[i] 表示以 i i i 结尾作为一段的最大权值,转移枚举 j j j 作为上一段结尾,得到转移方程:
f [ i ] = f [ j ] + v a l ( j + 1 , i ) f[i]=f[j]+val(j+1,i) f[i]=f[j]+val(j+1,i)
发现 v a l ( i + 1 , j ) val(i+1,j) val(i+1,j) 无法 O ( 1 ) O(1) O(1) 得到,比较好的计算方式是 枚举 j j j 的同时进行递推 或者 直接枚举 s i s_i si
但这并不符合斜率优化的要求,遇到复杂度的瓶颈,考虑某些最优化的性质
对于两个相邻段,如果选择的是同一大小 s i s_i si ?—— 显然合并成一段会更优
还是不好处理
继续考虑 (猜 ,将一段往两边延伸,若这一段选的是
s
i
s_i
si ,如果延伸的位置中没有
s
i
s_i
si,显然是没有意义的,不如将这些给其他段
那么就很好转移了:
f [ i ] = f [ j ] + ( S i − S j + 1 + 1 ) 2 × s i f[i]=f[j]+(S_i-S_{j+1}+1)^2\times s_i f[i]=f[j]+(Si−Sj+1+1)2×si ,其中 s i = s j + 1 s_i=s_{j+1} si=sj+1
化成斜率优化的形式:
f [ j ] + a j + 1 × ( S j + 1 − 1 ) 2 = 2 a i S i × ( S j + 1 − 1 ) − a i S i 2 + f [ i ] f[j]+a_{j+1}\times (S_{j+1}-1)^2=2\ a_iS_i\times (S_{j+1}-1)-a_iS_i^2+f[i] f[j]+aj+1×(Sj+1−1)2=2 aiSi×(Sj+1−1)−aiSi2+f[i]
对于每种大小 s i s_i si ,决策集合不同,我们直接考虑 1 e 4 1e4 1e4 个 d e q u e deque deque
要求 f [ i ] f[i] f[i] 最大值,即最大截矩,需要维护上凸包
再看 X X X 单增, Y Y Y 单增,斜率单增
上凸包中,斜率单减,为了维护决策集合高效,我们直接将队尾斜率小于当前 K K K 的点出队,然后取队尾即可
发现只有队尾存在元素的变动,我们直接用 v e c t o r vector vector 代替 d e q u e deque deque ,优化空间
本题告诉我们:
- 解决不了的 dp 复杂度问题靠性质来解决
- 斜率优化要灵活, d e q u e deque deque 只是一种 较 通用的实现方式,具体要根据 : X , Y , K X,Y,K X,Y,K 的增减性来确定
来总结一下斜率优化常犯小错误吧:
- 决策是 q [ t t ] q[tt] q[tt] 而不是 t t tt tt,调用时应该 C [ q [ t t ] ] C[q[tt]] C[q[tt]] 而非 C [ t t ] C[tt] C[tt]
- 使用 define 时一定要加 括号, define 的意义是 等效替代 而非 返回值
- f [ ] f[] f[] 初值需要赋 inf 时,将决策加入队列前考虑是否合法(即 != inf ),否则相乘直接爆掉了
- 斜率相乘可能会爆,开 l o n g l o n g longlong longlong
- 初值除了不合法状态要 inf ,对于 j = 0 j=0 j=0 的决策 f f f 值要特别注意