题解 P3642 【[APIO2016]烟火表演】

· · 题解

超神!!!根本想不到啊!

我可是理解了好久好久的喵呜。。。(σ-`д・´)

题解比代码还长.jpg

/*
*   令f(x)表示将某个点为根的子树中所有叶子到它的距离置为x的最小代价;
*   发现它是一个下凸函数,并且是线性的,即一次函数;
*   形象地理解一下,最小值左边的部分,大部分是由原来的长度减去值得到;
*   最小值右边的部分,大部分是由原来的长度加上值得到;
*   考虑一个点u,从所有儿子v转移过来;
*   过程分两步:
*   <1> 将所有v的凸包加上u-v这条边;
*   <2> 将加完边的凸包合并;
*   假设现在要加一条边w,f(x)在x属于[L,R]是取到最小值,要从f(x)转移到f'(x);
*   分四种情况:
*   +================================================================+
*   1.x<=L          f'(x)=f(x)+w;
*   原来这部分都是减去值得到,现在加了一条边之后仍要得到这个值x;
*   所以也必须要减去值才能满足,故直接减去w,将这条边变为0;
*   2.L<=x<=L+w     f'(x)=f(L)+w-(x-L)
*   这一段是从x=L开始的斜率为-1的一次函数;
*   由于之前将这条边减为0了,现在一点点加回去,相应的代价就逐渐减少1;
*   3.L+w<=x<=R+w   f'(x)=f(L)
*   将原先的取到最小值的部分转移到了这一段;
*   4.R+w<=x        f'(x)=f(L)+(x-R)-w
*   这一段是从x=R+w开始的斜率为1的一次函数;
*   原因是到了这里以后,每次只要在w这条边上+1即可;
*   +================================================================+
*   总结一下即,将[L,R]一段向右平移,[0,L]一段向上平移;
*   中间插入一段斜率为-1的线段,再将右边改为斜率为1的直线;
*   合并操作只要将函数相加即可;
*   考虑怎么维护:
*   不失一般性,认为每一个拐点都能使斜率+1;
*   ///观察///:
*   - 每次加边后,会让凸包最右端斜率为1,即一次合并后最右端斜率即为儿子个数;
*   - 凸包左边一段借据已知,即f(0)=边权和;
*   据此推算凸包表达式:
*   设此时拐点个数为p,根的儿子个数为d,总边权和为s;
*   则f(x)在0处的表达式为:f(x)=-(d-p)*x+s;
*   从小到大扫描每个拐点就能得到整个凸包的表达式;
*   我们现在只关心拐点的横坐标位置;
*   考虑用可合并大根堆存储拐点;
*   于是 <1> 就变成了:从堆中弹出点直到斜率为0,再将[L,R]右移以后的两个拐点加入;
*   <2> 就直接合并即可;
*   最后计算答案只需找到斜率为0的那一段计算代价即可;
*   神题啊!
*/
#include<bits/stdc++.h>
#define rep(i, x, y) for(int i = (x); i <= (y); i++)
#define per(i, x, y) for(int i = (x); i >= (y); i--)
#define N 600010
#define ll long long
using namespace std;
int n, m, tot, fa[N], w[N], d[N], rt[N];
ll sum;
struct heap {//左偏树,维护大根堆
    int l, r, dis; ll v;//v存当前点的横坐标值
} tr[N];
inline int merge(int x, int y) {
    if(!x || !y) return x+y;
    if(tr[x].v < tr[y].v) swap(x, y);
    tr[x].r = merge(tr[x].r, y);
    if(tr[tr[x].l].dis < tr[tr[x].r].dis) swap(tr[x].l, tr[x].r);
    if(!tr[x].r) tr[x].dis = 0; else tr[x].dis = tr[tr[x].r].dis+1;
    return x;
}
inline int pop(int x) { return merge(tr[x].l, tr[x].r); }
int main() {
    scanf("%d%d", &n, &m);
    rep(i, 2, n+m) {
        scanf("%d%d", &fa[i], &w[i]);
        sum += w[i]; d[fa[i]] ++;//d[]儿子个数,sum边权和
    }
    per(i, n+m, 2) {
        ll l = 0, r = 0;//l,r保存当前的斜率为0的一段
        if(i <= n) {
            while(--d[i]) rt[i] = pop(rt[i]);//不停弹出最大的点,直到斜率为0;点数刚好是儿子数
            r = tr[rt[i]].v; rt[i] = pop(rt[i]);
            l = tr[rt[i]].v; rt[i] = pop(rt[i]);
        }
        tr[++tot].v = l+w[i]; tr[++tot].v = r+w[i];
        rt[i] = merge(rt[i], merge(tot, tot-1));
        rt[fa[i]] = merge(rt[fa[i]], rt[i]);//合并儿子
    }
    while(d[1]--) rt[1] = pop(rt[1]);
    while(rt[1]) { sum -= tr[rt[1]].v; rt[1] = pop(rt[1]); }
    printf("%lld\n", sum);
    return 0;
}