【APIO2016】烟火表演
先前的题目对 slope trick 的认识还不深刻,这题可以看出一个完整的构建过程。
题目描述
给定一棵有根树,根为 \(1\) ,边带权,修改边权的代价时修改值与原值差的绝对值,求让所有叶子到根距离相等的最小代价。
\(1 \leq n \leq 3 \times 10^5,1 \leq w \leq 10^9\) 。
算法解析
首先有朴素 dp,设 \(f_{i,x}\) 为 \(i\) 点子树所有叶子到 \(i\) 距离都为 \(x\) 的最小代价:
但是复杂度过大,考虑优化,发现 \(f_i(x)\) 看作函数,就是一个下凸函数(不太会证)。
而且每次修改的斜率可能是 \(\Theta(1)\) 的。
这启发我们用 slope trick 做,现在研究被合并到父亲时需要做什么变化:
这里我们想象儿子是一个图像,父亲是一个图像,要将儿子的图像 \(x\) 坐标与父亲对齐后加上去。设 \(x\) 是父亲的横坐标,\(L,R\) 是儿子图像上最低点的左右端。
-
\(x \leq L\) :\(p\) 往左走,\(|w + p - x|\) 最多减小 \(1\) ,但是 \(f_v(p)\) 增加至少 \(1\) ,不优,所以取 \(p = x\) 时最优, 代价是 \(f_{v,x} + w\)。
-
\(L < x \leq L + w\) :发现如果 \(p\) 取 \(L\) 时 \(f_v(p)\) 取最小值,但是 \(x \leq w + p\) ,所以绝对值函数取不到最小值,相应地,要将 \(p\) 往左移一些,但是由于 \(f_v(p)\) 斜率的原因,还是不优,所以 \(p = L\) ,代价是 \(f_{v,L} + (w + L - x)\) 。
-
\(L + w < x \leq R + w\) :\(p = x - w\) 时绝对值和 \(f_v(p)\) 都取最小值,两全其美,代价是 \(f_{v,x - w}\) 。
-
\(x > R + w\) ,同 \(1\) ,无法向右走,\(p = R\) ,所以代价是 \(f_{v,R} + x - R - w\) 。
观察这个图形发生了什么?
众所周知的是,slope trick 中我们存多个拐点代表斜率此时增加 \(1\) 。但是不知道初始值,也就是 \(f_v(0)\) ,如果知道初始值和初始斜率,就可以递推出整段函数值。
所以我们假定我们现在知道初始值和初始斜率,这样后面的就更好理解。
-
1操作相当于将 \(x \leq L\) 的 \(f_{v,x}\) 部分向上平移 \(w\) ,直接初始值 \(+w\) 即可。
-
2操作相当于在刚刚的基础上,从 \(x = L\) 开始到 \(x = L + w\) 的一条斜率为 \(-1\) 的直线,由于原来这一段是平的。所以这里斜率为 \(-1\) 。
-
3操作继承原来最小值,计算发现初始值抬高了 \(w\) ,但是前面那段斜率为 \(-1\) 的直线正好下降了 \(w\) ,所以值直接不变。
-
4操作相当于在3的基础上,从 \(R + w\) 开始在后面连一条斜率为 \(1\) 的直线,直接删掉后面所有拐点,再加一个即可。
然后要将这个凸壳加到父亲上,这个使用可并堆即可,本文用了左偏树。
具体修改凸壳就是:删掉后面的一些点,在 \(L + w,R + w\) 两处插入两个拐点即可。
至于这个 "一些",儿子每次合并上来,后面会增加一个,所以数儿子数量,合并所有儿子时只留一个斜率 \(+1\) 的射线。
对于 \(f_1(0)\) 的值,发现直接是所有边权的和;初始斜率也好计算,堆的最后一个拐点后斜率为 \(+1\) ,向前推即可。
时间复杂度 \(\Theta(n \log n)\) 。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 6e5 + 5;
struct Heap{
int lc,rc,val,dis;
}t[N];
int rt[N],n,m,tot = 0,fa[N],C[N],deg[N];
inline int merge(int x,int y)
{
if(!x || !y) return x + y;
if(t[x].val < t[y].val) swap(x,y);
t[x].rc = merge(t[x].rc,y);
if(t[t[x].rc].dis > t[t[x].lc].dis) swap(t[x].rc,t[x].lc);
t[x].dis = t[t[x].rc].dis + 1;
return x;
}
inline int new_node(int v) {++tot; t[tot].lc = t[tot].rc = t[tot].dis = 0; t[tot].val = v; return tot;}
inline void push(int x,int v) {rt[x] = merge(rt[x],new_node(v));}
inline int top(int x) {return t[rt[x]].val;}
inline void pop(int x) {rt[x] = merge(t[rt[x]].lc,t[rt[x]].rc);}
signed main()
{
cin>>n>>m;
for(int i = 2;i <= n + m;i++)
{
cin>>fa[i]>>C[i];
deg[fa[i]]++;
}
for(int i = n + m;i >= 2;i--)
{
int L = 0,R = 0;
if(i <= n)
{
while(--deg[i]) pop(i);
R = top(i); pop(i);
L = top(i); pop(i);
}
push(i,L + C[i]); push(i,R + C[i]);
rt[fa[i]] = merge(rt[fa[i]],rt[i]);
}
while(--deg[1]) pop(1);
pop(1);
int ans = 0;
for(int i = 1;i <= n + m;i++) ans += C[i];
while(rt[1]) ans -= top(1),pop(1);
cout<<ans;
return 0;
}