【凸包维护-可并堆】LGP3642[APIO2016]烟火表演

【题目】
原题地址
题目大意:给你一棵带边权的有根树,你可以任意修改树的边权,问最少修改总和为多少的边权,能使得根节点到所有叶子节点的距离相同。

【题目分析】
暴力思考以后发现就是一个凸包合并之类的,但是这个合并很耐人寻味。

【解题思路】
APIO的题真是太妙了啊!

首先我们设f(i,x)为点i在它所有叶子节点深度为x时的最小代价,
我们可以发现这是一个下凸函数,而且是一次的,而且相邻两端斜率变化为1.
显然在斜率为0的时候取到最优值,所以对于每个点,实际上我们要做的就是合并它的所有儿子的凸包。
设取到最小值的区间为 [L,R] [ L , R ] ,然后我们有下面这个东西。

H(x)=H(x)+wH(L)+w(xL)H(L)H(L)+(xR)wxLLxL+wL+wxR+wR+wx H ′ ( x ) = { H ( x ) + w x ≤ L H ( L ) + w − ( x − L ) L ≤ x ≤ L + w H ( L ) L + w ≤ x ≤ R + w H ( L ) + ( x − R ) − w R + w ≤ x

想了很久想到这的时候我个人发现很不可做,因为插进来一堆点,节点数会很多。
然后sc告诉我splay合并是log^2,,启发式合并也是log^2,但是启发式合并顺序的splay是log的。
就感觉异常难打,sc大佬勇敢地开始打了,而我还在思索。

次日我又在研究这个凸包的性质,思考能不能用简单的数据结构维护凸包。
看到上面的合并凸包实际上只会改变最优值左边的凸包,因此最优值右边的凸包是没用的。
又发现实际上我们并不需要知道凸包的实际形态,只需要记住每个折点的位置,我们就能知道整个函数。
接着问题就在于怎么维护这些折点位置。
我们发现我们合并凸包的时候,我们一定是将两个最右端斜率为0的凸包进行合并,两个凸包的合并,其实等价于将一个凸包向左平移,然后再将斜率为-1,0,1的三段插入凸包。
插入以后会怎么样呢?我们发现凸包最右边的斜率最多为1。那么一个节点的所有子节点的凸包合起来,它的凸包最右边的斜率最多就是(子节点个数)
这样我们就可以简单地用一个堆来维护凸包的折点了!
简单来说,就是将所有凸包合并上去父节点,弹出右边(子节点个数)的节点,然后父节点再添加两个节点。

最后取出所有1节点上所有的点,然后依次减掉就行了。

wxh:
f(0)= f ( 0 ) = ∑ 树边权,然后斜率每次-1直到0
那么从右往左减就可以了
sum=p[i] s u m − = p [ i ] 可以理解为把 p[i] p [ i ] 的斜率=-1的贡献算进去,然后前面所有直线斜率-=1

【参考代码】

#include<bits/stdc++.h>
#define mkp(x,y) make_pair(x,y)
using namespace std;

typedef long long LL;
typedef long double ldb;
typedef pair<int,int> pii;
const int N=6e5+10;

int n,m,cnt,tot;
int fa[N],len[N],rt[N],du[N];
LL sum,p[N]; 

LL read()
{
    LL ret=0,f=1;char c=getchar();
    while(!isdigit(c)){if(c=='-')f=0;c=getchar();}
    while(isdigit(c)){ret=(ret<<1ll)+(ret<<3ll)+(c^48);c=getchar();}
    return f?ret:-ret;
}

struct Tnode
{
    int l,r,dis;
    LL val;
};
Tnode q[N];

int merge(int x,int y)
{
    if(!x || !y) 
        return x+y;
    if(q[x].val<q[y].val) 
        swap(x,y);
    q[x].r=merge(q[x].r,y);
    if(q[q[x].l].dis<q[q[x].r].dis)
        swap(q[x].l,q[x].r);
    q[x].dis=(q[x].r?q[q[x].r].dis+1:0);
    return x;
} 

int pop(int x)
{
    return merge(q[x].l,q[x].r);
}

int main()
{
    freopen("LGP3642.in","r",stdin);
    freopen("LGP3642.out","w",stdout);

    n=read();m=read();
    for(int i=2;i<=n+m;++i)
    {
        fa[i]=read();len[i]=read();
        sum+=len[i];du[fa[i]]++;
    }

    for(int i=n+m;i>1;--i)
    {
        LL l=0,r=0;
        if(i<=n)
        {
            while(--du[i])
                rt[i]=pop(rt[i]);
            l=q[rt[i]].val;rt[i]=pop(rt[i]); 
            r=q[rt[i]].val;rt[i]=pop(rt[i]);
        }
        q[++tot].val=l+len[i];q[++tot].val=r+len[i];
        rt[i]=merge(rt[i],merge(tot,tot-1));
        rt[fa[i]]=merge(rt[fa[i]],rt[i]);   
    }

    while(du[1]--)
        rt[1]=pop(rt[1]);
    while(rt[1])
        p[++cnt]=q[rt[1]].val,rt[1]=pop(rt[1]);
    for(int i=1;i<=cnt;++i) 
        sum-=p[i];
    printf("%lld\n",sum); 

    return 0;
} 

【总结】
中间思考的时候我也无聊,打了一发splay启发式合并,然后就弃疗了,就想出来了。
这个故事告诉我们数据结构太毒瘤了要多思考。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值