Slope Trick 总结
Slope Trick 总结
Slope Trick
Slope Trick 用于维护凸性的分段一次函数,且每一段一次函数的斜率满足为整数且级大小为 \(O(n)\)。
使用 Slope Trick 可以方便地求函数的最值、对后缀取最值、给全局加上一次函数或绝对值函数,要保证操作前后函数都为凸性。
算法概述
以下我们默认函数为下凸,即斜率递增。我们首先维护一个起始点 \(x_0\) 的 \(f(x_0)\) 以及往后一段直线的 \(k_0\)。
然后如果之后在 \(x\) 处往后斜率增加了 \(\Delta k\),那么往数据结构(堆)里插入 \(\Delta k\) 个 \(x\)。那么就有以下操作。
- 求最值:找到数据结构中 \(k=0\) 的 \(x\),则这个 \(x\) 就是函数上的最值。
- 全局加直线:对 \(k_0\) 加上斜率,并维护 \(f(x_0)\) 即可。
- 全局加绝对值函数:对 \(k_0\) 加上斜率,并维护 \(f(x_0)\),同时往数据结构中插入绝对值函数的断点。
- 对后缀取最小值:把 \(k=0\) 以后的点删掉即可。
P4597 序列 sequence - 洛谷
设 \(f_{i,j}\) 表示第 \(i\) 个位置选了 \(j\) 的最小操作数,有如下转移:
那么每次加上绝对值函数,然后把后缀取最小值。
思考一下可以发现,具体实现上就是先往堆里插入两个 \(a_i\),然后弹掉一个堆顶。
最后求出 \(x\) 后,可以遍历一遍函数求值。实现:
int n;
int a[N],f[N];
int f0,k0;
priority_queue<int> q;
int b[N],c[N];
signed main(){
read(n);
fo(i,1,n) read(a[i]),b[i]=a[i];
sort(b+1,b+1+n);
int len=unique(b+1,b+1+n)-b-1;
fo(i,1,n) c[i]=lower_bound(b+1,b+1+len,a[i])-b;
f0=a[1]-b[1],k0=-1;
q.push(c[1]);
fo(i,2,n) {
f0+=a[i]-b[1],k0--;
q.push(c[i]),q.push(c[i]);
while(Size(q)>-k0) q.pop();
}
vi t; while(Size(q)) t.pb(q.top()),q.pop();
int at=b[1];
fd(i,Size(t)-1,0) {
f0+=(b[t[i]]-at)*k0;
++k0,at=b[t[i]];
}
write(f0);
return 0;
}
P4331 [BalticOI 2004] Sequence 数字序列 - 洛谷
首先递增很难搞,有一个技巧,我们开始先把 \(a_i\) 全部减去 \(i\),这样就转化为了不下降的问题,最后输出时再加上 \(i\) 即可。
还是设 \(f_{i,j}\) 为第 \(i\) 数选了 \(j\),转移同理。那么我们要输出方案怎么办呢,我们考虑记录每个 \(i\) 操作完后最值 \(x\) 为 \(c_i\)。
那么如果 \(c_i\le c_{i+1}\) 显然 \(f_{i,c_i}\) 可以转移到 \(f_{i+1,c_{i+1}}\)。否则由于函数是凸的,所以最优情况下是第 \(i\) 次选 \(f_{i,c_{i+1}}\) 转移到第 \(f_{i+1,c_{i+1}}\)。
所以我们对 \(c_i\) 求后缀 \(\min\) 得到的数组就是 \(b_i\) 了。实现如下:
const int N=1e6+5;
int n,a[N],b[N];
int f0,k0;
priority_queue<int> q;
signed main(){
read(n);
fo(i,1,n) read(a[i]),a[i]-=i;
k0=-1,f0=a[1]+N;
q.push(a[1]);
b[1]=a[1];
fo(i,2,n) {
k0--,f0+=a[i]+N;
q.push(a[i]),q.push(a[i]);
q.pop();
b[i]=q.top();
}
int res=abs(a[n]-b[n]);
fd(i,n-1,1) b[i]=min(b[i+1],b[i]),res+=abs(a[i]-b[i]);
write(res,'\n');
fo(i,1,n) write(b[i]+i,' ');
return 0;
}
P3642 [APIO2016] 烟花表演 - 洛谷
设 \(f_i(x)\) 表示点 \(i\) 子树内的烟花从 \(i\) 开始还需要 \(x\) 秒同时爆炸的最小代价。
假设我们已经求出了 \(f_i\),考虑 \(i\) 前面再新增一条原长 \(val\) 的导线 \(f\) 会怎么改变,那么有如下转移:
设 \([L,R]\) 为 \(f_i\) 中斜率为 \(0\) 的那一段(即取最小值的那一段),那么有结论:
解释一下。对于第一条,发现当 \(w=0\) 时最优,因为前面的点一定比 \(x\) 的点更高。
对于第二条,即从 \(L\) 开始到 \(L+val\) 的斜率为 \(-1\) 的直线,因为此时令 \(x-w=L\) 时最优。
对于第三条,令 \(w=val\) 即可从 \([L,R]\) 转移过来。
对于第四条,与第二条同理,是从 \(R+val\) 开始的斜率为 \(1\) 的直线。
求出增加一条导线后的 \(f'\) 后,把一个点所有儿子的 \(f'\) 合并即可得到当前点的 \(f\)。****
使用可并堆实现,时间复杂度 \(O(n\log n)\)。参考代码:
const int N=3e5+5,Q=6e6;
#define int ll
int n,m;
vp G[N];
int lc[Q],rc[Q],dis[Q],val[Q],sz[Q],tot;
void maintain(int x) {
if(dis[lc[x]]<dis[rc[x]]) swap(lc[x],rc[x]);
dis[x]=dis[rc[x]]+1;
sz[x]=1+sz[lc[x]]+sz[rc[x]];
}
int merge(int x,int y) {
if(!x||!y) return x|y;
if(val[x]<val[y]) swap(x,y);
rc[x]=merge(rc[x],y);
maintain(x);
return x;
}
void push(int &x,int y) {val[++tot]=y,sz[tot]=1; x=merge(x,tot);}
int top(int x) {return val[x];}
void pop(int &x) {x=merge(lc[x],rc[x]);}
void Print(int x) {
cerr<<val[x]<<' ';
assert(dis[lc[x]]>=dis[rc[x]]);
if(lc[x]) Print(lc[x]);
if(rc[x]) Print(rc[x]);
}
void print(int x) {
Print(x), cerr<<'\n';
}
int rt[N];
int L[N],R[N],B[N],K[N];
void dfs(int x) {
for(auto [v,w]:G[x]) {
if(v>n) {
L[v]=R[v]=B[v]=w;
K[v]=-1;
push(rt[v],w),push(rt[v],w);
}
else {
dfs(v);
while(sz[rt[v]]>-K[v]) R[v]=top(rt[v]),pop(rt[v]);
L[v]=top(rt[v]);
B[v]+=w, pop(rt[v]);
push(rt[v],L[v]+w),push(rt[v],R[v]+w);
}
rt[x]=merge(rt[x],rt[v]),K[x]+=K[v],B[x]+=B[v];
}
}
signed main(){
read(n,m);
fo(i,2,n+m) {
int u,v; read(u,v);
G[u].pb({i,v});
}
dfs(1);
while(sz[rt[1]]>-K[1]) pop(rt[1]);
vi t; while(sz[rt[1]]) t.pb(top(rt[1])),pop(rt[1]);
int at=0;
fd(i,Size(t)-1,0) {
B[1]+=(t[i]-at)*K[1];
at=t[i],++K[1];
}
write(B[1],'\n');
return 0;
}

浙公网安备 33010602011771号