APIO2016 烟火表演
给定一棵树,带边权。\(1\) 的代价可以使某边权 \(\pm 1\)。求最小代价使从根到叶子距离都相等。
\(n\le 3\times 10^5,w_e\le 10^9\)。
\(f_u(x)\) 表示 \(u\) 的子树内把 \(u\) 到叶子的距离都变成 \(x\) 的最小代价。\(F_u(x)\) 表示 \(u\) 的子树内把 \(fa[u]\) 到叶子的距离都变成 \(x\) 的最小代价。
写出转移方程。
其中 \(len\) 表示 \(c\) 和 \(fa[c]\) 的边长度。
引理:\(f_v()\) 是下凸函数,\(F_c()\) 是下凸函数。
证明:
当 \(F_c\) 是下凸的,试证明 \(f_{fa[c]}\) 下凸。
\(f_{fa[c]}()=\sum F_c()\)。考虑二阶导数,\(F_c\) 的二阶导为正,则 \(f_{fa[c]}()\) 的二阶导等于 \(F_c()\) 二阶导求和,也是正的。所以 \(f_{fa[c]}()\) 是下凸的。
当 \(f_c\) 是下凸的,试证明 \(F_c\) 下凸。
记 \(g(d)=|d-len|\)。
我们把这些图像都画一下。
观察到 \(f(x-1)\) 相较于 \(f(x)\) 右移了一个单位长度;\(g(1)\) 相较于 \(g(0)\) (如果 \(len\ge 1\) 的话)向下移了一个单位长度。
所以 \(g(1)+f(x-1)\) 比 \(g(0)+f(x)\) 向右下方移了 \(1\) 格(如果 \(len\ge 1\));容易想到,当 \(d>len\) 之后,\(g(d)+f(x-d)\) 比 \(g(d-1)+f(x-d+1)\) 向右上方移了 \(1\) 格。
当取了 \(\min\) 之后(\(F_c\))是什么样的?
保留 \(g(0)+f(x)\) 最低点及其左侧的拐点 \(x_0\),然后从 \(x_0\sim x_0+len\) 都是一条斜率 \(-1\) 的直线,\(x_0+len\sim +\infty\) 都是一条斜率 \(+1\) 的直线。
显然这还是一个下凸函数。(其实这个证明很不严谨,感性理解吧)
基于证明,我们尝试维护 \(f_c\) 和 \(F_c\) 的图像。因为是凸的,维护拐点以及最左侧的起始点即可。
第一个问题是用什么维护拐点。用一个可重集维护,每一个元素都记录一个拐点;当一个元素重复出现,比如出现了 \(x\) 次,表示这个拐点到上一个拐点的斜率,相比于上一个拐点到上上个拐点的斜率,增加了 \(x\)。
最左侧的起始点,就是 \((0,f_c(0))\) 或者 \((0,F_c(0))\),容易求。
第二个问题是 \(F_{son}\rightarrow f_{fa}\) 时,拐点是如何变化的。
容易想到,只要把所有 \(son\) 的 \(F\) 图像拐点,全部合并(merge)起来即可。注意到从 \(f_c\) 最后一个拐点到 \(+\infty\) 的斜率是 \(c\) 的儿子数。
第三个问题是 \(f_c\rightarrow F_c\) 时,拐点是如何变化的。
观察上面证明的图像变化。
- 把斜率 \(>0\) 的全部改成斜率 \(1\),也就是把斜率 \(>0\) 的拐点全部 pop 了。
- 把斜率 \(=0\) 的那一段,向右平移 \(len\) 格。在做完 \(1\) 后,最靠右的两个拐点一定是斜率 \(0\) 的那一段。把它们取出来,都 \(+len\) 再放回去即可。
- "斜率 \(=0\) 的那一段的左端点" 到 "它左侧的拐点" 的斜率改成 \(-1\)。其实在做 \(2\) 的时候就已经顺便做了。
到这里已经可以做了。但是我们可以再多观察一个性质,更加优美地写代码。
因为每个 \(F\) 相邻拐点的斜率都 \(\le 0\),所以当产生斜率 \(\ge 1\) 的直线,必然是两个 \(F\) 合并了,而且必然产生且只产生一个。
所以 \(f_c\rightarrow F_c\) 进行 1 的时候,要把斜率 \(>0\) 的拐点全部 pop 了。这种拐点的个数就是 \(c\) 的儿子个数 \(-1\)。
维护拐点的集合,因为只会从右边删,用左偏树即可。
注意到我们其实并不需要同时维护 \(f\) 和 \(F\) 的图像。对于一个结点,它的 \(f\) 只在更新 \(F\) 的时候起作用,\(F\) 只在更新 \(f\) 的时候起作用,所以我们只维护 \(F\) 的图像。拿一个结点的 \(F\) 更新父结点的 \(f\),然后舍弃这个结点的 \(F\)。等到处理父结点的时候,再把 \(f\) 变成 \(F\)。注意在处理 \(1\) 的时候,不要把 \(f_1\) 也弄成 \(F_1\) 了。
当我们拥有了 \(f_1\),找到 \(f_1\) 的图像最低点。其函数值就是答案。
但是这里还有一个方法,可以简化我们求函数值的过程。首先 \(f_1(0)\) 就是所有边权的和,非常简单。\(f_1(0)\) 减去 "斜率 \(=0\) 的那一段的左端点及它左侧的所有拐点" 的 \(x\) 坐标之和,就是最低点的函数值。
注意到拐点总个数是 \(O(n)\) 的,所以复杂度 \(O(n\log n)\)。
点击查看代码
#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;
}