Slope Trick

据说 Slope Trick 和反悔贪心本质相同。

Slope Trick

对于一类二维 DP 问题,将状态 \(f_{i,j}\) 改写为函数形式 \(f_i(j)\),如果函数 \(f_i(x)\) 对于每个固定的 \(i\) 都是 \(x\) 的凸函数,则可以尝试用 Slope Trick 维护。

\(f_i(x)\) 视为函数后,我们考虑维护这个函数的差分 \(\Delta f_i(x)=f_i(x)-f_i(x-1)\);再分析并用数据结构维护\(f_{i-1}(x)\) 得到 \(f_i(x)\) 的过程中,斜率的变化答案的变化,从而避免求出所有 \(f_i(x)\) 的实际取值,降低复杂度。

「斜率」与「差分」

因为大多数题目的 DP 状态都只在整点取值,因此「差分」和「斜率」没有区别。为了适应 Slope Trick 这个名字,统一称之为「斜率」。

在实际维护上,一般有维护拐点和维护斜率两种方向,依据题目而选择。

凸函数

整数集上凸函数

定义函数 \(f:\Z\rightarrow\R\cup\set{\pm\infty}\) 为凸函数,当且仅当对于 \(\forall x\in\Z\),有 \(f(x)-f(x-1)\leq f(x+1)-f(x)\)

即斜率单调不降的函数,与之对应的有凹函数斜率单调不增,因此凹函数也可以称之为上凸。

实数集上凸函数

严谨的定义是任意两点连线都在函数图象的上方,即若函数 \(f:\mathbf R\rightarrow\mathbf R\cup\{\pm\infty\}\) 对于所有 \(x,y\in\mathbf R\)\(\alpha\in(0,1)\) 都满足:

\[f(\alpha x+(1-\alpha)y)\leq\alpha f(x)+(1-\alpha)f(y) \]

就称函数 \(f(x)\) 为凸函数,其中钦定 \(\pm\infty\) 乘以任何正实数或是加上任何实数都等于其自身,且对于任何实数 \(x\in\mathbf R\) 都有 \(-\infty<x<+\infty\)

可以参考此图:

对于凸函数 \(f(x)\),总有凹函数 \(-f(x)\)。因此对于凹函数,可以将其转化为凸函数处理。

如果 \(f(x)\) 的定义域仅是 \(\R\) 的子集,可以将其拓展为 \(\R\) 上的函数:

\[\tilde f(x)= \begin{cases} f(x)&x\in\operatorname{dom}f\\ +\infty&x\not\in\operatorname{dom}f \end{cases} \]

\(f(x)\) 为凸函数,当且仅当 \(\tilde f(x)\) 为凸函数。

常数、一次函数、绝对值函数等都是凸函数。

凸函数的变换

这是一些可以使用的凸函数的变换方法,实际题目中,也可以不运用这些方法,直接根据 DP 式推导斜率变化过程。当然,卷积下确界很好用就是了。

非负线性组合

对于凸函数 \(f(x),g(x)\)\(\alpha,\beta\geq0\),有凸函数 \(\alpha f(x)+\beta g(x)\),且:

\[\Delta(\alpha f(x)+\beta g(x))=\alpha\Delta f(x)+\beta\Delta g(x) \]

如果维护了 \(f(x),g(x)\) 的斜率,\(\alpha f(x)+\beta g(x)\) 的斜率只需要逐段合并即可。

在维护斜率的问题中,往往其中一个函数的形式比较简单,此时可以通过懒标记的方式降低修改复杂度。

在维护拐点的问题中,要计算 \(\alpha f(x)+\beta g(x)\) 的斜率拐点,只需要将 \(f(x),g(x)\) 的拐点合并即可。

卷积下确界

其实只是名字吓人。

对于函数 \(f(x)\),其下确界定义为 \(\inf f(x)\)\(\inf f(x)\) 是满足 \(f(x)\geq\inf f(x)\)\(\inf f(x)\) 最大的一个常数。在 Slope Trick 中,一般可直接理解为最小值 \(\min\),因为 DP 式的答案一般不能是一个趋近于极限但是取不到值的东西。

对于函数 \(f(x),g(x)\),其卷积下确界定义为:

\[h(x)=\inf_{y\in R}\left(f(y)+g(x-y)\right) \]

\(f(x),g(x)\) 都是凸函数,则 \(h(x)\) 也是凸函数。

在 Slope Trick 中,\(g(x)\) 一般是根据 DP 式构造的函数。

对于凹函数,与之对应的还有上确界 \(\sup f(x)\) 和卷积上确界 \(\displaystyle\sup_{y\in R}\left(f(y)+g(x-y)\right)\),在 Slope Trick 中一般可直接理解为最大值 \(\max\)

如果 \(f(x),g(x)\) 都是分段线性函数,那么一个非常好的性质是 \(h(x)\) 也为分段线性函数,且 \(h(x)\) 的斜率段集可以视作 \(f(x),g(x)\) 的斜率段的并集(可重)。

几何上,记 \(f(x)\) 函数图象上方的所有点组成的图象为 \(\operatorname{epi}f(x)\),则有 \(\operatorname{epi}h(x)\)\(\operatorname{epi} f(x),\operatorname{epi} g(x)\) 的闵可夫斯基和;由此可证明 \(h(x)\) 的斜率段为 \(f(x),h(x)\) 的斜率段的并集。

最值操作

两个凸函数 \(f(x),g(x)\) 的最大值 \(\max(f(x),g(x))\) 仍为凸函数,但最小值 \(\min(f(x),g(x))\) 不一定。

一些最小值操作可以转化为卷积下确界,如 \(\displaystyle\min_{y\in[x-a,x-b]}f(y)\) 可以转化为:

\[\begin{aligned} \min_{y\in[x-a,x-b]}f(y)&=\inf_{y\in\R}\left(f(y)+\tilde g(x-y)\right)\\ g(x)&=0(x\in[b,a]) \\ \tilde g(x)&= \begin{cases} 0&x\in[b,a]\\ +\infty&x\not\in[b,a] \end{cases} \end{aligned} \]

维护拐点

一般当斜率的范围较小,且每次变化的时候增加的量都相同时,可以维护拐点,而避免维护斜率本身。

考虑维护 \(f(x)\) 拐点 \(\xi_s\leq\xi_{s+1}\leq\xi_{s+2}\leq\cdots\leq\xi_{-1}\leq\xi_1\leq\cdots\leq\xi_{t-1}\leq\xi_t\),钦定 \(f(\xi_{-1})=f(\xi_1)\)\([\xi_{-1},\xi_1]\) 段内斜率为 \(0\)。之后每向左一个拐点,钦定斜率减小 \(1\);每向右一个拐点,钦定斜率增大 \(1\)

luogu P4597 序列 sequence

CF13C 数据加强版。

给定一个长度为 \(n\) 的序列 \(a_1,a_2,a_3,\cdots,a_n\),每次操作可以把某个数 \(+1\)\(-1\),求把序列变成非降序列的最小操作次数。

\(1\leq n\leq5\times10^5,\vert a_i\vert\leq10^9\)


\(f_{i,j}\) 表示 \(a_i\leftarrow j\) 的时候,\(a_1\sim a_i\) 合法的最小代价。

不难得到:

\[f_{i,j}=\min_{k\leq j}f_{i-1,k}+\vert a_i-j\vert \]

时间复杂度 \(\mathcal O(nV)\)

\(i=1\)\(f_i(x)=\vert a_i-x\vert\) 显然具有凸性。前缀 \(\min\) 是凸函数,绝对值也是凸函数,因此 \(f_i(x)\) 是凸函数。

维护 \(\xi_i\),为 \(f_i(x)\)\(k=0\) 的线段的右端点,答案即 \(f_n(\xi_n)\)

先考虑 \(f_{i-1}(x)\rightarrow f_i(x)\) 的时候,斜率段是如何变化的。

考虑前缀 \(\min\) 可以转化为卷积下确界,合并斜率集的时候会加入斜率为 \(0\) 的一段并向右无限延伸,因此只需要讨论斜率 \(\leq0\) 的情况。

考虑先加 \(\vert a_i-x\vert\),再做前缀 \(\min\),记 \(k=f_{i-1}(x)-f_{i-1}(x-1)\),分类讨论:

  • \(k\leq0,x\leq a_i\)

    \[\begin{aligned} f_i(x)-f_i(x-1)&=(f_{i-1}(x)+a_i-x)-(f_{i-1}(x-1)+a_i-x+1)\\ &=f_{i-1}(x)-f_{i-1}(x-1)-1 \end{aligned} \]

    斜率要 \(-1\)

  • \(k\leq 0,a_i<x\)

    \[\begin{aligned} f_i(x)-f_i(x-1)&=(f_{i-1}(x)-a_i+x)-(f_{i-1}(x-1)-a_i+x-1)\\ &=f_{i-1}(x)-f_{i-1}(x-1)+1 \end{aligned} \]

    斜率要 \(+1\)

因此加 \(\vert a_i-x\vert\) 等价于把 \((-\infty,a_i]\) 斜率增加 \(-1\)\([a_i,+\infty)\) 增加 \(+1\)

考虑如何维护这个东西,答案显然是 \(f_n(\xi_n)\);取前缀 \(\min\) 等价于把右边推平为 \(k=0\),因此我们只关心 \(k\leq0\) 的线段。所以其实 \(k\geq0\) 没什么用。

容易发现斜率每次变化都是 \(1\),且不会超过 \(n\),因此考虑维护拐点。(事实上也可以平衡树维护,但是比较难写。)

维护一个大根堆,其元素为线段的右端点,一个右端点可以重复多次,其线段的斜率为其需要完全弹出的次数的相反数。

  • \(a_i\geq\xi_{i-1}\) 时,答案不变。

    \(a_i\) 插入,因为 \(f_i(x)\) 是一个凸函数,此时 \(a_i\) 在最右侧,不影响其余取值,并且天然让其他线段的斜率减去 \(1\)

  • \(a_i<\xi_{i-1}\) 时。

    因为斜率要增加 \(1\),所以弹出最右侧的 \(\xi_{i-1}\)

    但是还要加入 \(a_i\) 作为拐点,考虑加入 \(a_i\) 的影响:

    • \(x\leq a_i\) 时,此时斜率因为弹出 \(\xi_{i-1}\) 再加入 \(a_i\),实际上没有变化。
    • \(a_i<x\) 时,斜率本身应当 \(+1\),弹出 \(\xi_{i-1}\) 恰好满足。

    因此为了让 \(x\leq a_i\) 的斜率符合条件,再加入一次 \(a_i\) 即可。

    记录当前 \(\textit{ans}=f_i(\xi_i)\),则弹出 \(\xi_{i-1}\) 时,还要更新答案 \(\textit{ans}\leftarrow\textit{ans}+\xi_{i-1}-a_i\)。因为整个函数图象在 \(k=0\) 的右端点 \(\xi_{i-1}\rightarrow\xi_i\) 的过程中,\(\xi_{i-1}\) 处的斜率为 \(0\)。这两个东西的纵坐标相等,带入解析式,\(\xi_{i-1}>a_i\),有:

    \[f_i(\xi_i)=f_i(\xi_{i-1})=f_{i-1}(\xi_{i-1})+\xi_{i-1}-a_i \]

由此即可 \(\mathcal O(n\log n)\) 维护。

参考代码
//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
using namespace std;
typedef long long ll;
constexpr const int N=5e5;
int n,a;
int main(){
	/*freopen("test.in","r",stdin);
	freopen("test.out","w",stdout);*/
	
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	
	cin>>n;
	priority_queue<int>q;
	ll ans=0;
	for(int i=1;i<=n;i++){
		cin>>a;
		q.push(a);
		if(a<q.top()){
			ans+=q.top()-a;
			q.pop();
			q.push(a);
		}
	}
	cout<<ans<<'\n';
	
	cout.flush();
	
	/*fclose(stdin);
	fclose(stdout);*/
	return 0;
}

维护斜率

CF865D Buy Low Sell High

\(i\) 天的股价为 \(p_i\),每天只能买卖至多一股,求第 \(n\) 天时的最大收益。

\(1\leq n\leq3\times10^5\)


\(f_{i,j}\) 为第 \(i\) 天持 \(j\) 股时的收益,有:

\[f_{i,j}=\max(f_{i-1,j-1}-p_i,f_{i-1,j},f_{i-1,j+1}+p_i) \]

边界 \(f_{0,0}=0\),答案即 \(f_{n,0}\)

时间复杂度 \(\mathcal O\left(n^2\right)\)

考虑 \(f_i(x)\)\(i=0\) 时只有一个取值 \(f_0(0)=0\),扩展得:

\[\tilde f_0(x)= \begin{cases} 0&x\in\set0\\ -\infty&x\not\in\set0 \end{cases} \]

这是一个凹函数。

之后尝试把 \(f_i(x)\) 写为卷积上确界:

\[\begin{aligned} f_i(x)&=\max_{y\in\R}(f_{i-1}(y)+\tilde g(x-y))\\ g(x)&=\begin{cases} p_i&x=-1\\ 0&x=0\\ -p_i&x=1 \end{cases}\\ \tilde g(x)&=\begin{cases} p_i&x=-1\\ 0&x=0\\ -p_i&x=1\\ -\infty&\text{otherwise.} \end{cases} \end{aligned} \]

\(\tilde g(x)\) 为凹函数,则 \(f_i(x)\) 也为凹函数。

之后考虑卷积上确界,合并斜率集,\(g(x)\) 的斜率段集为 \(\set{-p_i,-p_i}\)。斜率段的长度均为 \(1\)。其实也可以说是长度为 \(2\) 的斜率段集 \(\set{-p_i}\),但是为了方便维护,默认斜率段长度为 \(1\)

但是这样卷出来的 \(f_i(x)\) 有点小问题,在 \(x=-1\) 的时候出现了 \(f_i(-1)=f_{i-1}(0)+g(-1)\),但是这应当是不存在的。也就是说,这个点 \((-1,f_i(-1))\) 是不合法的,应当删除。

因为 \(f_i(x)\) 的凹性,直接删除最左侧斜率最大的长度为 \(1\) 的斜率段即可。

此时再考虑答案 \(f_i(0)\) 如何维护。注意到弹出的 \(f_i(-1)=f_{i-1}(0)+g(-1)=f_{i-1}(0)+p_i\)。令弹出的斜率为 \(k=f_i(0)-f_i(-1)\),维护 \(\textit{ans}=f_i(0)\),更新只需要:

\[\textit{ans}\leftarrow\textit{ans}+p_i+k \]

于是可以维护。

参考代码
//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
using namespace std;
typedef long long ll;
constexpr const int N=5e5;
int n,a;
int main(){
	/*freopen("test.in","r",stdin);
	freopen("test.out","w",stdout);*/
	
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	
	cin>>n;
	priority_queue<int>q;
	ll ans=0;
	for(int i=1;i<=n;i++){
		cin>>a;
		q.push(-a);
		q.push(-a); 
		ans+=a+q.top();
		q.pop();
	}
	cout<<ans<<'\n';
	
	cout.flush();
	
	/*fclose(stdin);
	fclose(stdout);*/
	return 0;
}

题外话

Hex_233 让 CodeX 跑的 Slope Trick 板子:

struct SlopeTrick{
	priority_queue<T>L;
	priority_queue<T,vector<T>,greater<T>>R;
	T addL=0,addR=0;
	T min_f=0;
	T topL(){
		return L.top()+addL;
	}
	T topR(){
		return R.top()+addR;
	}
	void pushL(T x){
		L.push(x-addL);
	}
	void pushR(T x){
		R.push(x-addR);
	}
	void add_const(T c){
		min_f+=c;
	}
	// f(x) += max(x - a, 0)
	void add_x_minus_a(T a){
		if (!L.empty()&&topL()>a){
			T l=topL();
			min_f+=l-a;
			L.pop();
			pushL(a);
			pushR(l);
		}else{
			pushR(a);
		}
	}
	// f(x) += max(a - x, 0)
	void add_a_minus_x(T a) {
		if (!R.empty()&&topR()<a){
			T r=topR();
			min_f+=a-r;
			R.pop();
			pushR(a);
			pushL(r);
		} else {
			pushL(a);
		}
	}
	void add_abs(T a){
		add_a_minus_x(a);
		add_x_minus_a(a);
	}
	// f(x) <- min_{y <= x} f(y)
	void clearRight(){
		while(!R.empty()){
			R.pop(); 
		}
	}
	// f(x) <- min_{y >= x} f(y)
	void clearLeft(){
		while(!L.empty()){
			L.pop();
		}
	}
	// f(x) <- f(x - a)
	void shift(T a){
		addL+=a;
		addR+=a;
	}
	// f(x) <- min_{x-b <= y <= x-a} f(y)
	void shift(T a,T b){
		addL+=a;
		addR+=b;
	}
	T query(){
		return min_f;
	}
};
posted @ 2026-07-01 16:56  TH911  阅读(2)  评论(0)    收藏  举报