Bootstrap

【斜率优化】笔记

3. 斜率优化

斜率优化是一个非常神奇的一种优化DP的方法,一般情况下,当我们的状态转移方程式中出现了形如 x y xy xy 的式子,那这道题多半就是一道斜率优化

本篇帖子将浅谈一下斜率优化,顺便写篇水题的题解

3.1. 前置知识:斜率

在这里插入图片描述

设直线AB的解析式为 y = k x + b y=kx+b y=kx+b,则 k k k 为直线AB的斜率

怎么在不知道AB解析式的情况下求解该直线的斜率呢?

A ( x a , y a ) , B ( x b , y b ) A(x_a,y_a),B(x_b,y_b) A(xa,ya),B(xb,yb),则直线AB的斜率为 y a − y b x a − x b \dfrac{y_a-y_b}{x_a-x_b} xaxbyayb

记住这个公式

3.2. 斜率优化介绍

在了解了斜率是什么后,我们就以一道例题作为小白鼠样例来讲解一下斜率优化

Eg [ZJOI2007] 仓库建设

3.2.1. 朴素DP

定义状态:

dp[i]为在第 i i i 号工厂必须建立仓库的费用,则易得状态转移方程式为:

d p [   i   ] = min ⁡ { d p [   j   ] + ∑ k = j + 1 i − 1 ( p [   k   ] × ( x [   i   ] − x [   k   ] ) ) } + c [   i   ] ( 0 ≤ j < i , 下同 ) = min ⁡ { d p [   j   ] + ∑ k = j + 1 i − 1 ( p [   k   ] × x [   i   ] − p [   k   ] × x [   k   ] ) } + c [   i   ] = min ⁡ { d p [   j   ] + x [   i   ] × ∑ k = j + 1 i − 1 ( p [   k   ] ) − ∑ k = j + 1 i − 1 ( p [   k   ] × x [   k   ] ) } + c [   i   ] \begin{aligned}dp[\ i\ ]&=\min\{dp[\ j\ ]+\sum\limits_{k=j+1}^{i-1}(p[\ k\ ]\times(x[\ i\ ]-x[\ k\ ]))\}+c[\ i\ ](0\le j<i,\text{下同})\\&=\min\{dp[\ j\ ]+\sum\limits_{k=j+1}^{i-1}(p[\ k\ ]\times x[\ i\ ]-p[\ k\ ]\times x[\ k\ ])\}+c[\ i\ ]\\&=\min\{dp[\ j\ ]+x[\ i\ ]\times\sum\limits_{k=j+1}^{i-1}(p[\ k\ ])-\sum\limits_{k=j+1}^{i-1}(p[\ k\ ]\times x[\ k\ ])\}+c[\ i\ ]\end{aligned} dp[ i ]=min{dp[ j ]+k=j+1i1(p[ k ]×(x[ i ]x[ k ]))}+c[ i ](0j<i,下同)=min{dp[ j ]+k=j+1i1(p[ k ]×x[ i ]p[ k ]×x[ k ])}+c[ i ]=min{dp[ j ]+x[ i ]×k=j+1i1(p[ k ])k=j+1i1(p[ k ]×x[ k ])}+c[ i ]

s u m p [   i   ] = ∑ k = 1 i p [   i   ] , s u m [   i   ] = ∑ k = 1 i ( p [   i   ] × x [   i   ] ) sump[\ i\ ]=\sum\limits_{k=1}^ip[\ i\ ],sum[\ i\ ]=\sum\limits_{k=1}^i(p[\ i\ ]\times x[\ i\ ]) sump[ i ]=k=1ip[ i ],sum[ i ]=k=1i(p[ i ]×x[ i ]),则可以进一步化简状态转移方程式:

d p [   i   ] = min ⁡ { d p [   j   ] + x [   i   ] × ( s u m p [   i − 1   ] − s u m p [   j   ] ) − ( s u m [   i − 1   ] − s u m [   j   ] ) } + c [   j   ] dp[\ i\ ]=\min\{dp[\ j\ ]+x[\ i\ ]\times(sump[\ i-1\ ]-sump[\ j\ ])-(sum[\ i-1\ ]-sum[\ j\ ])\}+c[\ j\ ] dp[ i ]=min{dp[ j ]+x[ i ]×(sump[ i1 ]sump[ j ])(sum[ i1 ]sum[ j ])}+c[ j ]

在我们没有学习斜率优化之前,我们只能将此方程式简化到这种程度,时间复杂度为 O ( n 2 ) O(n^2) O(n2)

现在,正式介绍一下斜率优化

3.2.2. 斜率优化优化DP

我们设有两个可选择的决策点 j , k ( k < j < i ) j,k(k<j<i) j,k(k<j<i),并假设 j j j 决策点优于 k k k 决策点

根据上面的状态转移方程式,可以得到下述式子:

d p [   j   ] + x [   i   ] × ( s u m p [   i − 1   ] − s u m p [   j   ] ) − ( s u m [   i − 1   ] − s u m [   j   ] ) + c [   i   ] < d p [   k   ] + x [   i   ] × ( s u m p [   i − 1   ] − s u m p [   k   ] ) − ( s u m [   i − 1   ] − s u m [   k   ] ) + c [   i   ] dp[\ j\ ]+x[\ i\ ]\times(sump[\ i-1\ ]-sump[\ j\ ])-(sum[\ i-1\ ]-sum[\ j\ ])+c[\ i\ ]<dp[\ k\ ]+x[\ i\ ]\times(sump[\ i-1\ ]-sump[\ k\ ])-(sum[\ i-1\ ]-sum[\ k\ ])+c[\ i\ ] dp[ j ]+x[ i ]×(sump[ i1 ]sump[ j ])(sum[ i1 ]sum[ j ])+c[ i ]<dp[ k ]+x[ i ]×(sump[ i1 ]sump[ k ])(sum[ i1 ]sum[ k ])+c[ i ]

很长,是不是?

接下来,我们对这个式子进行化简:

d p [   j   ] + x [   i   ] × ( s u m p [   i − 1   ] − s u m p [   j   ] ) − ( s u m [   i − 1   ] − s u m [   j   ] ) + c [   i   ] < d p [   k   ] + x [   i   ] × ( s u m p [   i − 1   ] − s u m p [   k   ] ) − ( s u m [   i − 1   ] − s u m [   k   ] ) + c [   i   ] d p [   j   ] − x [   i   ] × s u m p [   j   ] + s u m [   j   ] < d p [   k   ] − x [   i   ] × s u m p [   k   ] + s u m [   k   ] d p [   j   ] + s u m [   j   ] − ( d p [   k   ] + s u m [   k   ] ) < x [   i   ] × ( s u m p [   j   ] − s u m [   k   ] ) \begin{aligned}dp[\ j\ ]+x[\ i\ ]\times(sump[\ i-1\ ]-sump[\ j\ ])-(sum[\ i-1\ ]-sum[\ j\ ])+c[\ i\ ]&<dp[\ k\ ]+x[\ i\ ]\times(sump[\ i-1\ ]-sump[\ k\ ])-(sum[\ i-1\ ]-sum[\ k\ ])+c[\ i\ ]\\dp[\ j\ ]-x[\ i\ ]\times sump[\ j\ ]+sum[\ j\ ]&<dp[\ k\ ]-x[\ i\ ]\times sump[\ k\ ]+sum[\ k\ ]\\dp[\ j\ ]+sum[\ j\ ]-(dp[\ k\ ]+sum[\ k\ ])&<x[\ i\ ]\times(sump[\ j\ ]-sum[\ k\ ])\end{aligned} dp[ j ]+x[ i ]×(sump[ i1 ]sump[ j ])(sum[ i1 ]sum[ j ])+c[ i ]dp[ j ]x[ i ]×sump[ j ]+sum[ j ]dp[ j ]+sum[ j ](dp[ k ]+sum[ k ])<dp[ k ]+x[ i ]×(sump[ i1 ]sump[ k ])(sum[ i1 ]sum[ k ])+c[ i ]<dp[ k ]x[ i ]×sump[ k ]+sum[ k ]<x[ i ]×(sump[ j ]sum[ k ])

因为 k < j k<j k<j,所以 s u m p [   k   ] < s u m p [   j   ] sump[\ k\ ]<sump[\ j\ ] sump[ k ]<sump[ j ],因此该式子最终化简为:

d p [   j   ] + s u m [   j   ] − ( d p [   k   ] + s u m [   k   ] ) s u m p [   j   ] − s u m p [   k   ] < x [   i   ] \dfrac{dp[\ j\ ]+sum[\ j\ ]-(dp[\ k\ ]+sum[\ k\ ])}{sump[\ j\ ]-sump[\ k\ ]}<x[\ i\ ] sump[ j ]sump[ k ]dp[ j ]+sum[ j ](dp[ k ]+sum[ k ])<x[ i ]

同理,若 j j j 决策点劣于 k k k 决策点,最终会得到下述式子:

d p [   j   ] + s u m [   j   ] − ( d p [   k   ] + s u m [   k   ] ) s u m p [   j   ] − s u m p [   k   ] > x [   i   ] \dfrac{dp[\ j\ ]+sum[\ j\ ]-(dp[\ k\ ]+sum[\ k\ ])}{sump[\ j\ ]-sump[\ k\ ]}>x[\ i\ ] sump[ j ]sump[ k ]dp[ j ]+sum[ j ](dp[ k ]+sum[ k ])>x[ i ]

仔细看看左边的式子,是不是很眼熟?

联想一下斜率公式: y a − y b x a − x b \dfrac{y_a-y_b}{x_a-x_b} xaxbyayb

是的,左边的式子其实就是在表示斜率

为了方便表示,我们把左边的式子表示为 [   j , k   ] [\ j,k\ ] [ j,k ]

结合上面的两个式子,得出结论1:

[   j , k   ] < x [   i   ] [\ j,k\ ]<x[\ i\ ] [ j,k ]<x[ i ],则说明 j j j 决策点比 k k k 决策点更优,若 [   j , k   ] > x [   i   ] [\ j,k\ ]>x[\ i\ ] [ j,k ]>x[ i ],则说明 k k k 决策点比 j j j 决策点更优

这里,我们还要在推理一个结论2:

3.2.2.1.推理一个结论

先上结论

设有三个决策点 i , j , k ( k < j < i ) i,j,k(k<j<i) i,j,k(k<j<i),若 [   i , j   ] < [   j , k   ] [\ i,j\ ]<[\ j,k\ ] [ i,j ]<[ j,k ](即 i , j i,j i,j 所表示的斜率小于了 j , k j,k j,k 所表示的斜率),则 j j j 决策点一定不是最优决策点

换言之,如果出现了这种情况:

在这里插入图片描述

或者这种情况:

在这里插入图片描述

那么, j j j 决策点一定不是最优决策点

证明是显然的:

设当前所处理的点为 P P P

情况1: [   j , k   ] < x [   P   ] [\ j,k\ ]<x[\ P\ ] [ j,k ]<x[ P ]

由结论1可知: j j j 优于 k k k i i i 优于 j j j j j j 不是最优的

情况2: [   j , k   ] > x [   P   ]   and ⁡   [   i , j   ] > x [   P   ] [\ j,k\ ]>x[\ P\ ]\ \operatorname{and}\ [\ i,j\ ]>x[\ P\ ] [ j,k ]>x[ P ] and [ i,j ]>x[ P ]

由结论1可知: k k k 优于 j j j j j j 优于 i i i j j j 不是最优的

情况2: [   j , k   ] > x [   P   ]   and ⁡   [   i , j   ] < x [   P   ] [\ j,k\ ]>x[\ P\ ]\ \operatorname{and}\ [\ i,j\ ]<x[\ P\ ] [ j,k ]>x[ P ] and [ i,j ]<x[ P ]

由结论1可知: k k k 优于 j j j i i i 优于 j j j j j j 不是最优的

结论2得证

换言之,如果我们吧所有的非最优决策点扔掉,把所有的最优决策点连起来,应该长这样:

在这里插入图片描述

是的,一个凹包

凹包的有一个性质:斜率单调递增

因此,我们可以使用一个单调队列来维护斜率来保证斜率的单调性

那么,剩下的做法几乎和普通的单调队列优化一模一样了

代码时间:

#include<cstdio>
#include<algorithm>
using namespace std;
long long int n,dp[2000005],q[2000005],head,tail;
long long int x[2000005],p[2000005],c[2000005];
long long int sum[2000005],sump[2000005];
long long int Up(long long int j,long long int k){			//式子[i,j]的分子部分
	return dp[j]+sum[j]-dp[k]-sum[k];
}
long long int Down(long long int j,long long int k){			式子[i,j]的分母部分
	return sump[j]-sump[k];
}
int main(){
	scanf("%lld",&n);
	for(int i=1;i<=n;i++){
		scanf("%lld%lld%lld",&x[i],&p[i],&c[i]);
		sump[i]=sump[i-1]+p[i];
		sum[i]=sum[i-1]+x[i]*p[i];
	}
	for(int i=1;i<=n;i++){
		while(head<tail&&Up(q[head+1],q[head])<=x[i]*Down(q[head+1],q[head])){
			head++;
		}//如果当前队首的决策点已经不是最优的决策点了,就把它扔掉
		 //这里将除法换成了乘法,避免精度问题
		dp[i]=dp[q[head]]+x[i]*(sump[i-1]-sump[q[head]])-sum[i-1]+sum[q[head]]+c[i];			//用最优的决策点进行dp的转移
		while(head<tail&&Up(i,q[tail])*Down(q[tail],q[tail-1])<=Up(q[tail],q[tail-1])*Down(i,q[tail])){
			tail--;
		}//如果新加进来的点会打破凹包结构,就把当前队列里的最后一个点扔掉
		q[++tail]=i;			//加新点
	}
	printf("%lld",dp[n]);			//输出答案
	return 0;
}

现在,我们就完成了此题目。。。吗?

在这里插入图片描述

很明显,被Hack了

3.3. 关于这道题目的一些微操(现在可以把万恶的斜率优化扔一边了)

问题出在哪儿呢?

对于 100 % 100\% 100% 的数据,保证 1 ≤ n ≤ 1 0 6 1 \leq n \leq 10^6 1n106 0 ≤ x i , p i , c i < 2 31 0 \leq x_i,p_i,c_i < 2^{31} 0xi,pi,ci<231

如果光是这样,你还看不出来,那我就给一个特写镜头:

0 ≤ p i < 2 31 0\le p_i<2^{31} 0pi<231

换言之,有可能有一些没有货物的无效工厂

基于此,这里给出三组Hack

Input:
2
0 0 10
1 0 10
Output:
0
Reason:
没有建造仓库的必要
Input:
2
0 10 100
10 0 10
Output:
20
Reason:
在第二个工厂建一个仓库,并把一号工厂里的货物运过去
Input:
2
0 10 10
10 0 100
Output:
10
Reason:
在第一个工厂建一个仓库即可

对于第一个Hack,我们可以进行特判,如果所有的工厂都没有货物,就不用建仓库了

那第二,三个Hack怎么解决呢?

先上代码:

#include<cstdio>
#include<algorithm>
using namespace std;
long long int n,dp[2000005],q[2000005],head,tail,flag,ans=29999823190680;
long long int x[2000005],p[2000005],c[2000005];
long long int sum[2000005],sump[2000005];
long long int Up(long long int j,long long int k){
	return dp[j]+sum[j]-dp[k]-sum[k];
}
long long int Down(long long int j,long long int k){
	return sump[j]-sump[k];
}
int main(){
	scanf("%lld",&n);
	for(int i=1;i<=n;i++){
		scanf("%lld%lld%lld",&x[i],&p[i],&c[i]);
		if(p[i]){
			flag=1;
		}
		sump[i]=sump[i-1]+p[i];
		sum[i]=sum[i-1]+x[i]*p[i];
	}
	if(!flag){			//特判
		printf("0");
		return 0;
	}
	for(int i=1;i<=n;i++){
		while(head<tail&&Up(q[head+1],q[head])<=x[i]*Down(q[head+1],q[head])){
			head++;
		}
		dp[i]=dp[q[head]]+x[i]*(sump[i-1]-sump[q[head]])-sum[i-1]+sum[q[head]]+c[i];
		while(head<tail&&Up(i,q[tail])*Down(q[tail],q[tail-1])<=Up(q[tail],q[tail-1])*Down(i,q[tail])){
			tail--;
		}
		q[++tail]=i;
	}
	for(int i=n;i>=1;i--){
		ans=min(ans,dp[i]);			//下文解释
		if(p[i]){
			printf("%lld",ans);
			return 0;
		}
	}
	return 0;
}

为什么可以这样做呢?

首先,清楚一个事情:对于dp[i]而言, i i i 号工厂后面的所有工厂我们是暂未考虑的

假设最后一个有货物的工厂编号是 P P P,那么,dp[P-1]是没有考虑 P P P 号工厂的,而 P P P 号工厂因为有货物,我们必须考虑它,因此,我们的答案从dp[P]开始尝试更新,后面的所有没有货物的工厂因为可以用来建造仓库,所以是有价值的

因此,最终答案为 a n s = min ⁡ { d p [   i   ] } ( P ≤ i ≤ n ) ans=\min\{dp[\ i\ ]\}(P\le i\le n) ans=min{dp[ i ]}(Pin)

关于斜率优化,应该就是这些了

;