APIO2016 烟火表演

传送门

给定一棵树,带边权。1 的代价可以使某边权 ±1。求最小代价使从根到叶子距离都相等。
n3×105,we109


fu(x) 表示 u 的子树内把 u 到叶子的距离都变成 x 的最小代价。Fu(x) 表示 u 的子树内把 fa[u] 到叶子的距离都变成 x 的最小代价。

写出转移方程。

fv(x)=cson(v)Fc(x)

Fc(x)=mind{|lend|+fc(xd)}

其中 len 表示 cfa[c] 的边长度。

引理:fv() 是下凸函数,Fc() 是下凸函数。

证明:

Fc 是下凸的,试证明 ffa[c] 下凸。
ffa[c]()=Fc()。考虑二阶导数,Fc 的二阶导为正,则 ffa[c]() 的二阶导等于 Fc() 二阶导求和,也是正的。所以 ffa[c]() 是下凸的。

fc 是下凸的,试证明 Fc 下凸。
g(d)=|dlen|

Fc(x)=min{g(0)+fc(x),g(1)+fc(x1),}

我们把这些图像都画一下。

观察到 f(x1) 相较于 f(x) 右移了一个单位长度;g(1) 相较于 g(0) (如果 len1 的话)向下移了一个单位长度。

所以 g(1)+f(x1)g(0)+f(x) 向右下方移了 1 格(如果 len1);容易想到,当 d>len 之后,g(d)+f(xd)g(d1)+f(xd+1) 向右上方移了 1 格。

当取了 min 之后(Fc)是什么样的?

保留 g(0)+f(x) 最低点及其左侧的拐点 x0,然后从 x0x0+len 都是一条斜率 1 的直线,x0+len+ 都是一条斜率 +1 的直线。

显然这还是一个下凸函数。(其实这个证明很不严谨,感性理解吧)


基于证明,我们尝试维护 fcFc 的图像。因为是凸的,维护拐点以及最左侧的起始点即可。

第一个问题是用什么维护拐点。用一个可重集维护,每一个元素都记录一个拐点;当一个元素重复出现,比如出现了 x 次,表示这个拐点到上一个拐点的斜率,相比于上一个拐点到上上个拐点的斜率,增加了 x

最左侧的起始点,就是 (0,fc(0)) 或者 (0,Fc(0)),容易求。

第二个问题是 Fsonffa 时,拐点是如何变化的。
容易想到,只要把所有 sonF 图像拐点,全部合并(merge)起来即可。注意到从 fc 最后一个拐点到 + 的斜率是 c 的儿子数。

第三个问题是 fcFc 时,拐点是如何变化的。
观察上面证明的图像变化。

  1. 把斜率 >0 的全部改成斜率 1,也就是把斜率 >0 的拐点全部 pop 了。
  2. 把斜率 =0 的那一段,向右平移 len 格。在做完 1 后,最靠右的两个拐点一定是斜率 0 的那一段。把它们取出来,都 +len 再放回去即可。
  3. "斜率 =0 的那一段的左端点" 到 "它左侧的拐点" 的斜率改成 1。其实在做 2 的时候就已经顺便做了。

到这里已经可以做了。但是我们可以再多观察一个性质,更加优美地写代码。
因为每个 F 相邻拐点的斜率都 0,所以当产生斜率 1 的直线,必然是两个 F 合并了,而且必然产生且只产生一个。
所以 fcFc 进行 1 的时候,要把斜率 >0 的拐点全部 pop 了。这种拐点的个数就是 c 的儿子个数 1

维护拐点的集合,因为只会从右边删,用左偏树即可。

注意到我们其实并不需要同时维护 fF 的图像。对于一个结点,它的 f 只在更新 F 的时候起作用,F 只在更新 f 的时候起作用,所以我们只维护 F 的图像。拿一个结点的 F 更新父结点的 f,然后舍弃这个结点的 F。等到处理父结点的时候,再把 f 变成 F。注意在处理 1 的时候,不要把 f1 也弄成 F1 了。

当我们拥有了 f1,找到 f1 的图像最低点。其函数值就是答案。

但是这里还有一个方法,可以简化我们求函数值的过程。首先 f1(0) 就是所有边权的和,非常简单。f1(0) 减去 "斜率 =0 的那一段的左端点及它左侧的所有拐点" 的 x 坐标之和,就是最低点的函数值。

注意到拐点总个数是 O(n) 的,所以复杂度 O(nlogn)

点击查看代码
#include <bits/stdc++.h>

using namespace std;
typedef long long ll;
const int N = 6e5 + 5;

struct Node {
	ll val;
	int l, r, d;
	
	Node() {
		val = l = r = 0;
		d = -1;
	}
	Node(ll v) {
		val = v;
		l = r = d = 0;
	}
};
int sz = 0;
Node a[N];
int new_Node(ll v) { //新建一个权值为v的结点 
	a[++sz] = Node(v);
	return sz;
}
int mrg(int L, int R) { //合并以L,R为根的左偏树,返回最终根结点 
	if (L == 0 || R == 0)
		return L + R;
	if (a[L].val < a[R].val)
		swap(L, R);
	a[L].r = mrg(a[L].r, R);
	if (a[a[L].l].d < a[a[L].r].d)
		swap(a[L].l, a[L].r);
	a[L].d = a[a[L].r].d + 1;
	return L;
}
int pop(int x) { //弹出x,顺便返回新堆的根结点 
	return mrg(a[x].l, a[x].r);
}

int rt[N]; //rt[i]记录结点i的左偏树的根 

int n, m;
int p[N], c[N]; //父结点,到父结点的边权 
int sons[N] = {}; //儿子个数 

int main() {
	cin >> n >> m;
	ll f10 = 0;
	for (int i = 2; i <= n + m; i++) {
		cin >> p[i] >> c[i];
		f10 += c[i];
		sons[p[i]]++;
	}
	
	for (int i = n + m; i >= 2; i--) {
		for (int j = 1; j < sons[i]; j++) //第一步:弹出所有斜率>0的 
			rt[i] = pop(rt[i]);
		//第二步:把斜率0的那一段向右平移len(c[i])
		ll R = a[rt[i]].val;
		rt[i] = pop(rt[i]);
		ll L = a[rt[i]].val;
		rt[i] = pop(rt[i]);
		rt[i] = mrg(rt[i], mrg(new_Node(L + c[i]), new_Node(R + c[i])));
		//第三步:右端改成斜率1的,已经完成
		
		//当前结点的F合并到父结点的f
		rt[p[i]] = mrg(rt[p[i]], rt[i]);
	}
	//把f1的斜率>0的也pop了,这样堆顶就是最低点
	for (int j = 1; j < sons[1]; j++)
		rt[1] = pop(rt[1]);
	rt[1] = pop(rt[1]); //把slope=0的右端点pop了
	while (rt[1]) {
		f10 -= a[rt[1]].val;
		rt[1] = pop(rt[1]);
	}
	cout << f10 << endl;
	return 0;
}
posted @   FLY_lai  阅读(12)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示