斜率优化 DP 总结

斜率优化 DP

顾名思义,运用数形结合的思想,将 DP 中求解的 最优值 问题 转化为 坐标系中相切问题,根据直线的斜率来求解

一般来说,斜率优化 DP 有几个前提:

  1. 朴素 DP 转移时需要枚举 决策,可以将决策的 O ( n ) O(n) O(n)枚举 优化到 O ( l o g ) O(log) O(log) 甚至 O ( 1 ) O(1) O(1),而 不可以在状态上进行优化
  2. 将外层的枚举变量当成常量后,转移方程可以写成 阶段 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]=0j<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]SC[j]=T[i]C[j]+f[i]C[i]T[i]SC[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)=kX(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 ;
}

其实本题能这么 斜率优化 还是有一些性质的:

  1. 观察 X ( j ) = C [ j ] X(j)=C[j] X(j)=C[j] ,同样单增,这是斜率优化的 必要条件,即新加入决策点的横坐标务必单增,否则可能需要一些横纵坐标互换,转化坐标系的技巧或者高级数据结构,简单说就是暂时搞不了
  2. 所求直线的斜率单调递增,这使得我们可以实时维护队头来确保最优决策,但是当斜率不单增时,就不得不使用二分了

例 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 性质 + 特性

[JSOI2011] 柠檬

分段式 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]+(SiSj+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+11)2=2 aiSi×(Sj+11)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 ,优化空间

本题告诉我们:

  1. 解决不了的 dp 复杂度问题靠性质来解决
  2. 斜率优化要灵活, d e q u e deque deque 只是一种 通用的实现方式,具体要根据 : X , Y , K X,Y,K X,Y,K 的增减性来确定

来总结一下斜率优化常犯小错误吧:

  1. 决策是 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]
  2. 使用 define 时一定要加 括号, define 的意义是 等效替代 而非 返回值
  3. f [ ] f[] f[] 初值需要赋 inf 时,将决策加入队列前考虑是否合法(即 != inf ),否则相乘直接爆掉了
  4. 斜率相乘可能会爆,开 l o n g l o n g longlong longlong
  5. 初值除了不合法状态要 inf ,对于 j = 0 j=0 j=0 的决策 f f f 值要特别注意
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值