【APIO2016】烟火表演

发布时间 2023-12-29 21:20:18作者: The_Last_Candy

先前的题目对 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} = \sum_v \min_{p \leq x} f_{v,p} + |w + p - 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;
}