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(x)\) 为凸函数,其中钦定 \(\pm\infty\) 乘以任何正实数或是加上任何实数都等于其自身,且对于任何实数 \(x\in\mathbf R\) 都有 \(-\infty<x<+\infty\)。
可以参考此图:
对于凸函数 \(f(x)\),总有凹函数 \(-f(x)\)。因此对于凹函数,可以将其转化为凸函数处理。
如果 \(f(x)\) 的定义域仅是 \(\R\) 的子集,可以将其拓展为 \(\R\) 上的函数:
称 \(f(x)\) 为凸函数,当且仅当 \(\tilde f(x)\) 为凸函数。
常数、一次函数、绝对值函数等都是凸函数。
凸函数的变换
这是一些可以使用的凸函数的变换方法,实际题目中,也可以不运用这些方法,直接根据 DP 式推导斜率变化过程。当然,卷积下确界很好用就是了。
非负线性组合
对于凸函数 \(f(x),g(x)\) 和 \(\alpha,\beta\geq0\),有凸函数 \(\alpha f(x)+\beta 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)\),其卷积下确界定义为:
若 \(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)\) 可以转化为:
维护拐点
一般当斜率的范围较小,且每次变化的时候增加的量都相同时,可以维护拐点,而避免维护斜率本身。
考虑维护 \(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\)。
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;
}
维护斜率
第 \(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\),扩展得:
这是一个凹函数。
之后尝试把 \(f_i(x)\) 写为卷积上确界:
\(\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)\),更新只需要:
于是可以维护。
参考代码
//#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;
}
};

浙公网安备 33010602011771号