前言
诚然,虽然它名字里带了"Slope",但是它不是斜率优化,而是一个比它还要难的东西(作者本人主观臆断)。
并且,关于 CF13C,有一点很多文章都没有提及,所以会有人看不懂为什么要这么做(作者本人亲身体验)。
理论部分
Slope Trick 是一种利用数据结构维护分段一次函数的技巧。
核心思想是通过维护拐点(斜率变化点)或斜率维护函数;如果需要还原每一段的函数,就需要维护 \(k,b\)。
当需要维护的函数具有以下性质时,可以使用 Slope Trick:
-
是一次(分段)函数;
-
函数连续;
-
是凸函数(即斜率单调);
-
斜率为整数。
主要应用场景是 dp 优化。
我强烈建议你在阅读之后的内容前先把上述性质抄在一张纸上,不然你很可能因为忘记函数具有这些性质而不理解为什么可以这么做。(主要是斜率单调)
我们可以举一个例子:\(f(x)=|x-3|\)。
那么它是符合上述性质的分段函数,且它的拐点序列为 \([3,3]\)。
再来一个例子:\(y=|x-3|+|x-5|+x-5\)。
这个函数也满足上述性质,且它的拐点序列为 \([3,3,5]\)。
一些进阶操作
我们观察到所有满足上述性质的函数都可以用上述形式表示,因此我们可以进行以下操作。
找最小值
我需要提醒你:这个函数的斜率单调。
因此当斜率为 \(0\) 时,函数的值最小。
当然,如果你需要找到这一段的左右端点,那也不难。
开两个堆 \(L,R\) 分别维护这一段左边和右边的拐点。
堆顶就是这一段的左右端点。
加一个满足性质的函数
维护 \(k,b\) 后将拐点集合合并即可。
至于维护 \(L,R\),只需要保证 \(L.size()=k\) 即可。
加一次函数
这里主要讲加一次函数后怎么维护 \(L,R\)。
注意到加了一次函数 \(y=kx+b(k>0)\) 后每一段的斜率都加了 \(k\),所以我们就把 \(k\) 个拐点从 \(L\) 移动到 \(R\)。(具体的移动方向需要结合图像确定)
前后缀取min
我需要提醒你:这个函数的斜率单调。
显然,当我们取到最小值后,前后缀最小值就不会再变化了。
所以我们取前缀(后缀)min时,就是把 \(k>0\) ( \(k<0\) )的部分去掉,在操作上就是只用 \(L\) 中(\(R\) 中)的拐点。
平移和翻转
维护 \(k,b\) 后打上给拐点打上标记即可。
来一道简单题
对,相信你已经在其他文章里过许多次了:CF13C。
我们定义 \(f_{i,j}\) 表示前 \(i\) 项不减,且将 \(a_i=j\) 的最小代价。容易列出:
然后我们令 \(g_{i,j}=\min \limits_{k \le j} f_{i-1,j}\),那么原式可以被改写为 \(f_{i,j}=g_{i,j}+|a_i-j|\)。答案就是 \(\min g_{n,i}\)。
然后我们观察到 \(F_i(j)=f_{i,j}\) 是一个凸函数。理由很简单:首先 \(F_0\) 显然是凸函数;然后 \(g_0\) 是 \(F_0\) 的前缀最小值,也是凸函数;然后 \(F_i\) 就是两个凸函数加起来的结果,也一定是一个凸函数。
至此我们已经判断 \(F_i\) 和 \(g_i\) 满足了使用 Slope Trick 的条件,然后我们看我们需要干什么:我们需要给 \(F_i\) 加上一个凸函数(绝对值函数),然后对其取前缀 min。因为答案就是 \(g_n\) 的最小值,所以我们只需要维护 \(g_n\) 的 \(L\) 栈顶的值即可。(因为 \(L\) 栈顶就是斜率为 \(0\) 的那一段函数的左端点,它的值就是函数最小值)
令 \(L\) 栈顶的值为 \(pos\),分类讨论绝对值函数 \(y=|x-a|\) 中 \(a\) 的取值范围:
当 \(pos \le a\) 时:
可以看到,大于 \(pos\) 那部分时没有意义的(因为肯定会在取前缀 min 时被去掉)。因此我们只需要加上一个与斜率负一抵消的拐点即可。
当 \(pos > a\):
此时,函数的两端都是有意义的。那么按维护的规则,我们要加入往 \(L\) 中加入两个 \(a\)。但是这是有一个问题:这样一来,原来斜率为 \(0\) 的段就变成了斜率为 \(1\) 的段,斜率为 \(-1\) 的段变成了斜率为 \(0\) 的段。所以我们还需要进行一些额外的维护操作。
我们来思考一下我们需要干什么:
-
维护 \(L\) 堆;
-
维护 \(g\) 的最小值。
维护 \(L\) 堆上文我们提到了,就是把 \(L\) 的栈顶移动到 \(R\) 堆里。接下来是维护 \(g\) 的最小值。注意到最小值的变化量就是绝对值函数在 \(pos\) 处的取值。这和 \(pos-a\) 在数值上相等。
参考实现:
const int N=5e5+100;
int n,a[N];
ll ans;
priority_queue<int,vector<int>,less<int> > q;
int main()
{n=read();For(i,1,n) a[i]=read();For(i,1,n){q.push(a[i]);if(a[i]<q.top()){q.push(a[i]);ans+=q.top()-a[i];q.pop();}}printf("%lld",ans);return 0;
}
这才是标准的题目
题目链接:P3642 [APIO2016] 烟花表演。
简要题意
给定一颗有根树,边有边权。你可以花费 \(1\) 的代价使任意一条边的边权减一或加一。询问使所有叶子到根的距离相等的最小代价。
分析
首先看上去就很 dp,于是考虑状态设计。设 \(f_{u,i}\) 表示使 \(u\) 子树内的所有叶子到 \(u\) 的距离为 \(i\) 的最小代价。
那么存在转移(其中 \(w_{u,v}\) 是边 \((u,v)\) 的边权,下文简写为 \(w\)):
注意到 dp 转移形如若干个绝对值之和,所以 \(F_u(i)=f_{u,i}\) 是一个凸函数。接下来考虑分类讨论:
我们约定 \(L,R\) 分别为 \(F_v\) 中斜率为 \(0\) 的一段的左右端点。
-
若 \(i < L\):考虑从 \(j\) 到 \(j-1\) 时函数值的增量,因为在 \((-\infty,L]\) 上斜率小于等于 \(-1\),因此 \(f_{v,i}\) 的增量不小于 \(1\);又因为 \(\lvert w-(i-j) \rvert\) 的增量至少为 \(-1\),因此这个变化不会变优,因此当 \(j=i\) 是函数值最小,有 \(F_u(i)=F_v(i)+w\);
-
若 \(i \ge L\):此时 \(j\) 可以取 \([L,R]\) 中的值,因此 \(F_v(j)\) 的部分一定最小,考虑怎么让绝对值函数的值尽可能地小。
-
若 \(j=i-w \in [L,R]\),那么此时绝对值函数的值可以取到 \(0\),因此有 \(F_u(i)=F_v(L)\)。
-
若 \(j=i-w < L\),那么此时 \(\lvert j-(i-w)\rvert= j-i+w\),因此有 \(F_u(i)=F_v(L)+L-i+w\)。
-
若 \(j=i-w > R\),那么此时 \(\lvert j-(i-w)\rvert=-j+i-w\),因此有 \(F_u(i)=F_v(L)+i-R-w\)。
-
整理后有:
然后就是体现到 Slope Trick 上(假设第一段函数为 \(y=kx+b\)):
-
当 \(i <L\) 时:等价于 \(b\) 加 \(w\);
-
当 \(L \le i < L+w\) 时:等价于在 \(L\) 处加入一段斜率为 \(-1\) 的直线;
-
当 \(L+w \le i \le R+w\) 时:等价于将 \([L,R]\) 平移到 \([L+w,R+w]\);
-
当 \(i > R+w\) 时:等价于将 \(R+w\) 之后的斜率都改为 \(1\)。
于是上述操作对拐点集合的影响为:去掉 \(L\) 及之后的拐点,并加入 \(L+w\) 和 \(R+w\)。
那么怎么找 \(L,R\) 呢?注意到每一次合并后,斜率为正的函数有且仅有一段(\(R+w\)),因此对于一个有 \(k\) 个儿子的点,它的 \(L,R\) 就是弹出 \(k-1\) 个拐点后剩下的那两个。
统计答案的时候我们注意到 \(F_1(0)\) 就是所有边权和,然后统计每一个拐点的贡献即可。
代码
#include<bits/stdc++.h>
#define inf 0x3f3f3f3f
#define Inf (1ll<<60)
#define For(i,s,t) for(int i=s;i<=t;++i)
#define Down(i,s,t) for(int i=s;i>=t;--i)
#define bmod(x) ((x)>=p?(x)-p:(x))
#define lowbit(x) ((x)&(-(x)))
#define End {printf("NO\n");exit(0);}
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
inline void ckmx(int &x,int y){x=(x>y)?x:y;}
inline void ckmn(int &x,int y){x=(x<y)?x:y;}
inline void ckmx(ll &x,ll y){x=(x>y)?x:y;}
inline void ckmn(ll &x,ll y){x=(x<y)?x:y;}
inline int min(int x,int y){return x<y?x:y;}
inline int max(int x,int y){return x>y?x:y;}
inline ll min(ll x,ll y){return x<y?x:y;}
inline ll max(ll x,ll y){return x>y?x:y;}
inline int read(){register int x=0,f=1;char c=getchar();while(c<'0' || '9'<c) f=(c=='-')?-1:1,c=getchar();while('0'<=c && c<='9') x=(x<<1)+(x<<3)+c-'0',c=getchar();return x*f;
}
void write(int x){if(x>=10) write(x/10);putchar(x%10+'0');
}
const int N=6e5+100;
int n,m,ls[N],rs[N],dist[N],fa[N],d[N],rt[N],tot;
ll ans,val[N],len[N];
int merge(int x,int y){if(!x || !y) return x|y;if(val[x]<val[y]) swap(x,y);rs[x]=merge(rs[x],y);if(d[ls[x]]<d[rs[x]])swap(ls[x],rs[x]);dist[x]=dist[rs[x]]+1;return x;
}
void insert(int& p,ll x){++tot,val[tot]=x;p=merge(p,tot);
}
void pop(int& p){p=merge(ls[p],rs[p]);
}
int main()
{
#if !ONLINE_JUDGEfreopen("test.in","r",stdin);freopen("test.out","w",stdout);
#endif dist[0]=-1;n=read(),m=read();For(i,2,n+m) fa[i]=read(),len[i]=read(),++d[fa[i]],ans+=len[i];Down(u,n+m,2){if(u<=n) while(d[u]>1) pop(rt[u]),--d[u];ll R=val[rt[u]];pop(rt[u]);ll L=val[rt[u]];pop(rt[u]);insert(rt[u],L+len[u]);insert(rt[u],R+len[u]);rt[fa[u]]=merge(rt[fa[u]],rt[u]);}while(d[1]) pop(rt[1]),--d[1];//这里不需要留 Rwhile(rt[1]) ans-=val[rt[1]],pop(rt[1]);printf("%lld",ans);return 0;
}