APIO2016 Fireworks
APIO2016 Fireworks
题意:
题解:
第一眼想到的应该是一个\(Dp\),我们记\(f_{u, i}\)表示\(u\)这个子树中,所有叶子节点到\(u\)的距离都为\(i\)的最小代价。考虑这个\(Dp\)函数的形状,发现在叶子节点中,这个\(Dp\)是呈\(V\)型的,在\(len_u\)取得最小值。仔细分析一下发现,几个\(V\)型函数叠起来,也一定是一个下凸的函数,并且是由一段段斜率递增的分段一次函数组成的。这里我们规定这个下凸函数中相邻的两个一次函数之间的斜率相差\(1\),即使可能存在两个区间\([a, b], [b, c]\)的斜率分别为\(1, 3\),我们也认为在\([b, b]\)这个长度为\(0\)的区间中存在一个斜率为\(2\)的曲线。具体为什么后面会提到。
我们考虑如何从点\(v\)转移到他的父亲\(u\),我们记\([L, R]\)为函数\(f_v\)中斜率为为\(0\)的区间,\(len\)为\((u, v)\)的边长,然后转移方程如下:
首先考虑第一个转移\(f_{u, i} = f_{v, i} + len \ \ (i \leq L)\)
由于\([L, R]\)区间斜率为\(0\),所以当\(i \leq L\)时,所在函数的斜率一定小于\(0\),假设\(f_{u, i}\)是从\(f_{v, k}\)转移过来的,由于\(k\)所在直线斜率小于\(0\),所以当转移点从\(k\)变成\(k + 1\)的时候,减少的代价一定大于等于\(1\),而此时的边长减少了\(1\),代价增加了\(1\),所以我们从\(k + 1\)转移到\(i\)一定不会变劣,那么最优的转移点一定就是\(f_{v, i}\),那么此时的新边的边长为\(0\),所以这次转移的代价就是\(len\)。
然后考虑$f_{u, i} = f_{v, l} + len - i + L \ \ (L \leq i \leq L +len) \( 这个是比较显然的,假设我们是从\)f_{v, k}\(转移过来的,那么新边的边长为\)i - k\(,这次转移的代价为\)len - i + k\(。而在\)[L, L + len]\(区间内的\)f_{v}\(的值,一定是非递减的,所以我们在\)k\(取最小为\)L$的时候一定是最优的。
然后是$f_{u, i} = f_{v, i - len} (L + len \leq i \leq R +len) \( 和第一种转移差不多的方法来考虑,在\)[L + len, R + len]$区间中函数的斜率一定大于等于\(0\),假设\(f_{u, i}\)是从\(f_{v, k}\)转移过来的,由于\(k\)所在直线斜率小于\(0\),所以当转移点从\(k\)变成\(k - 1\)的时候,减少的代价一定大于等于\(1\),而此时的边长增加了\(1\),代价减少了\(1\),所以从\(k - 1\)转移到\(i\)一定是更优的,那么最优转移点为\(i - len\),此时新边边长为\(len\),转移的代价为\(0\)
最后是\(f_{u, i} = f_{v, r} + i - R - len \ \ (i \geq R + len)\)
在\([R + len, + \infty]\)区间中的斜率一定大于等于\(1\),所以此时转移点从\(k\)变成\(k - 1\)的时候,减少的代价一定大于\(1\),而新边的边长增加了\(1\),所以此时的最优转移点是\(R\),新边的边长为\(R + len\),转移的代价为\(i - R - len\)。
现在我们知道了这些转移的方程了,考虑我们该怎么快速维护这个转移。观察这些转移的实质,第一个转移实际上是把\([- \infty,L]\)中的所有点上移\(len\)的距离,第二个转移实际上是在\([L, L + len]\)区间内插入一条斜率为\(-1\)的斜线,第三个转移实际上是把\([L, R]\)区间右移\(len\)的长度,第四个转移实际上是在\([R + len, + \infty]\)插入斜率为\(1\)的斜线。所以两个\(V\)型函数的合并相当于将斜率为\(0\)的一段右移,然后插入两端斜率为\(-1\)和\(1\)的函数。发现这个东西在有了之前我们规定的下凸函数中相邻的两个一次函数之间的斜率相差\(1\)的性质之后就可以维护了。我们在一个堆中记录当前函数的拐点,那么每有一个拐点,当前斜率就加\(1\)。
这样就会有一个比较显然的结论,就是每一个点的下凸函数的最大的斜率就是这个点的子节点个数。因为我们每次将子节点合并到父节点的时候,最右边斜率大于\(1\)的点都会被弹出,所以我们每次只会把一个斜率最大值为\(1\)的\(V\)型函数复合到父节点上,那么一个点有多少个子节点,那么最右边的斜率就是多少。这样每次弹出右边斜率大于\(1\)的点,然后合并到父节点上,我们就可以得到根节点的\(V\)型函数了。这个过程可以用任意一个可并堆来维护。
得到了根节点的函数之后,我们考虑如何计算。首先\(f_0\)一定是\(\sum_u len_u\),即所有边权之和。我们就希望得到\(f_L\)的值,这个值就是答案了。具体算法就是用\(f_0\)减去当前函数中所有斜率小于等于\(0\)的拐点的\(x\)坐标之和,证明如下:
我们记\(\{x_1, x_2, \cdots, x_n, L, R \}\)为当前根节点函数中所有拐点的\(x\)坐标,那么根据每经过一个拐点,斜率就增加\(1\)的性质,我们可以推出\(f_0 - f_L = (L - x_n) \times 1 + (x_n - x_{n - 1}) \times 2 + \cdots (x_2 - x_1) \times (n - 1) + (x_1 - 0) \times n\),这个经过化简可得\(f_0 - f_L = x_1 + x_2 + \cdots + x_n\),由此可以算出\(f_L\),这个即为答案。
Code
#pragma GCC optimize(2,"inline","Ofast")
#include <bits/stdc++.h>
using namespace std;
const int N = 3e5 + 50;
typedef long long ll;
namespace Heap {
struct node {
int ls, rs, dis;
ll v;
}t[N << 1];
int rt[N];
int tot;
int Merge(int x, int y) {
if(!x || !y) return x | y;
if(t[x].v < t[y].v) swap(x, y);
t[x].rs = Merge(t[x].rs, y);
t[x].dis = t[x].rs ? t[t[x].rs].dis + 1 : 0;
if(!t[x].ls || t[t[x].ls].dis < t[t[x].rs].dis) swap(t[x].ls, t[x].rs);
return x;
}
int Newnode(ll v) { t[++tot] = (node) { 0, 0, 0, v }; return tot; }
void Pop(int &rt) { rt = Merge(t[rt].ls, t[rt].rs); }
void Push(int Rt, ll v) { int o = Newnode(v); rt[Rt] = Merge(rt[Rt], o); }
}
int n, m;
ll ans = 0;
int sz[N], v[N], fa[N];
int main() {
scanf("%d%d", &n, &m);
for(int i = 2; i <= n + m; i++) scanf("%d%d", &fa[i], &v[i]), sz[fa[i]]++, ans += v[i];
for(int i = n + 1; i <= n + m; i++) {
Heap::Push(i, v[i]); Heap::Push(i, v[i]);
Heap::rt[fa[i]] = Heap::Merge(Heap::rt[fa[i]], Heap::rt[i]);
}
for(int i = n; i >= 2; i--) {
while(sz[i] > 1) sz[i]--, Heap::Pop(Heap::rt[i]);
ll R = Heap::t[Heap::rt[i]].v; Heap::Pop(Heap::rt[i]);
ll L = Heap::t[Heap::rt[i]].v; Heap::Pop(Heap::rt[i]);
R = R + v[i], L = L + v[i];
Heap::Push(i, L); Heap::Push(i, R);
Heap::rt[fa[i]] = Heap::Merge(Heap::rt[fa[i]], Heap::rt[i]);
}
while(sz[1]) sz[1]--, Heap::Pop(Heap::rt[1]);
while(Heap::rt[1]) ans -= Heap::t[Heap::rt[1]].v, Heap::Pop(Heap::rt[1]);
printf("%lld\n", ans);
return 0;
}