zoukankan      html  css  js  c++  java
  • Loj #2568. 「APIO2016」烟花表演

    Loj #2568. 「APIO2016」烟花表演

    题目描述

    烟花表演是最引人注目的节日活动之一。在表演中,所有的烟花必须同时爆炸。为了确保安全,烟花被安置在远离开关的位置上,通过一些导火索与开关相连。导火索的连接方式形成一棵树,烟花是树叶,如图 1所示。火花从开关出发,沿导火索移动。每当火花抵达一个分叉点时,它会扩散到与之相连的所有导火索,继续燃烧。导火索燃烧的速度是一个固定常数。图 1展示了六枚烟花 ({E_1, E_2, ldots, E_6 }) 的连线布局,以及每根导火索的长度。图中还标注了当在时刻 (0) 从开关点燃火花时,每一发烟花的爆炸时间。

    图 1

    图 1

    Hyunmin 为烟花表演设计了导火索的连线布局。不幸的是,在他设计的布局中,烟花不一定同时爆炸。我们希望修改一些导火索的长度,让所有烟花在同一时刻爆炸。例如,为了让图 1中的所有烟花在时刻 (13) 爆炸,我们可以像图 2中左边那样调整导火索长度。类似地,为了让图 1中的所有烟花在时刻 (14) 爆炸,我们可以像图 2中右边那样调整长度。

    图 2

    图 2

    修改导火索长度的代价等于修改前后长度之差的绝对值。例如,将图 1中布局修改为图 2,左边布局的总代价为 (6),而将图 1中布局修改为图 2右边布局的总代价为 (5)

    导火索的长度可以被减为 (0),同时保持连通性不变。

    给定一个导火索的连线布局,你需要编写一个程序,去调整导火索长度,让所有的烟花在同一时刻爆炸,并使得代价最小。

    输入格式

    所有的输入均为正整数。令 (N) 代表分叉点的数量,(M) 代表烟花的数量。分叉点从 (1)(N) 编号,编号为 (1) 的分叉点是开关。烟花从 (N+1)(N+M) 编号。

    输入格式如下:

    (N::M)

    (P_2::C_2)

    (P_3::C_3)

    (ldots)

    (P_N::C_N)

    (P_{N+1}::C_{N+1})

    (ldots)

    (P_{N+M}::C_{N+M})

    其中 (P_i) 满足 (1le P_i<i),代表和分叉点或烟花 (i) 相连的分叉点。(C_i) 代表连接它们的导火索长度 ((1le C_ile 10^9))。除开关外,每个分叉点和多于 (1) 条导火索相连,而每发烟花恰好与 (1) 条导火索相连。

    数据范围与提示

    子任务 1(7 分):(N=1,1 le M le 100)

    子任务 2(19 分):(1 le N+M le 300),且开关到任一烟花的距离不超过 (300)

    子任务 3(29 分):(1 le N+M le 5000)

    子任务 4(45 分):(1 le N+M le 3 imes 10^5)

    (\)

    参考博客

    (f_i(x))为使得(i)的子树中所有叶子到(i)距离为(x)所付出的代价。很明显(f_i(x))是个下凸的函数,而且中间有一整段斜率为(0)的区间,假设为([L,R])。我们先考虑已经得到了(f_u(x)),要加上(u)的父亲到(u)的那条边(长度为(w))后函数怎么变化。

    [f_{fa_u}(x)= egin{cases} f_u(x)+w & xleq L\ f_u(L)+w-(x-L) & Lleq xleq L+w\ f_u(L) & L+w <xleq R+w\ f_u(R)+x-w-R & R+w<x end{cases} ]

    假设最终这条边的边权为(w'),最优策略就是尽量使得(x-w'),也就是所有叶子到(u)的距离尽量往([L,R])靠。

    我们观察这个转移,相当于将(L)以左的部分抬高(w),然后接一条斜率为(-1)的直线,然后接一条斜率为(0)的直线,最后接斜率为(1)的直线。也就是说先将右端的斜率大于(0)的直线全部删除,再加入上述三条直线。

    假设我们已经完成了这个操作,于是就把这个函数加到(fa_u)的函数中。我们发现累加函数会使斜率逐渐增大。比如函数(1)(xgeq x_1)的部分斜率为(1),函数(2)(xgeq x_2)的部分斜率为(1)(x_1<x_2)),那么新函数在(x_1leq xleq x_2)的部分斜率为(1),在(x>x_2)的部分斜率为(2)

    所以我们只需要维护函数的拐点的横坐标就行了。考虑每合并一个儿子,函数的最大斜率都会(+1),所以函数最右端斜率(>0)的端点个数就是儿子的数量。具体实现可以用可并堆。

    最后考虑得到最终的函数后怎么算答案。明显答案就在那一段斜率为(0)的区间,但是我们只维护了横坐标。我们知道(f_1(0)=sum),其中(sum)表示所有的边长度之和。每次合并函数,最大斜率(+1),同理最小斜率也会(-1),所有函数左侧的斜率也是递减的。假设最左侧斜率(<0)的一堆端点有(k)个,分别为(x_1,x_2,ldots,x_k),那么答案就

    [sum-x_1*k+sum_{i=2}^k(x_i-x_{i-1})*(k-i+1)\ =sum-sum_{i=1}^kx_i ]

    代码:

    #include<bits/stdc++.h>
    #define ll long long
    #define N 600005
    
    using namespace std;
    inline int Get() {int x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9') {if(ch=='-') f=-1;ch=getchar();}while('0'<=ch&&ch<='9') {x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}return x*f;}
    
    int n,m;
    struct tree {
    	int ls,rs,key;
    	ll val;
    }tr[N<<1];
    int Merge(int a,int b) {
    	if(!a||!b) return a+b;
    	if(tr[a].val<tr[b].val) swap(a,b);
    	tr[a].rs=Merge(tr[a].rs,b);
    	if(tr[tr[a].ls].key<tr[tr[a].rs].key) swap(tr[a].ls,tr[a].rs);
    	tr[a].key=tr[a].rs?tr[tr[a].rs].key+1:0;
    	return a;
    }
    int Pop(int a) {return Merge(tr[a].ls,tr[a].rs);}
    
    int fa[N];
    int len[N];
    ll sum;
    int sn[N];
    int rt[N];
    int tot;
    int main() {
    	n=Get(),m=Get();
    	for(int i=2;i<=n+m;i++) {
    		fa[i]=Get(),len[i]=Get();
    		sum+=len[i];
    		sn[fa[i]]++;
    	}
    	for(int i=n+m;i>=2;i--) {
    		ll l=0,r=0;
    		if(i<=n) {
    			while(--sn[i]) rt[i]=Pop(rt[i]);
    			r=tr[rt[i]].val;rt[i]=Pop(rt[i]);
    			l=tr[rt[i]].val;rt[i]=Pop(rt[i]);
    		}
    		tr[++tot].val=l+len[i];
    		tr[++tot].val=r+len[i];
    		rt[i]=Merge(rt[i],Merge(tot-1,tot));
    		rt[fa[i]]=Merge(rt[fa[i]],rt[i]);
    	}
    	
    	while(sn[1]--) rt[1]=Pop(rt[1]);
    	while(rt[1]) {
    		sum-=tr[rt[1]].val;
    		rt[1]=Pop(rt[1]);
    	}
    	cout<<sum;
    	return 0;
    }
    
    
  • 相关阅读:
    maven打包额外的资源文件
    阿里巴巴的程序员等级
    sql是最成功的第四代语言
    nginx的配置与应用
    浏览器的同源策略与跨域问题的解决方案
    算法:二分查找(基础)
    动态类型语言和静态类型语言
    【VS开发】单文档中往视图中加入控件
    【VS开发】使用VS2010创建MFC ActiveX工程项目
    【VS开发】使用VS2010创建MFC ActiveX工程项目
  • 原文地址:https://www.cnblogs.com/hchhch233/p/10941732.html
Copyright © 2011-2022 走看看