Loj #2568. 「APIO2016」烟花表演

题目描述

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

Loj #2568. 「APIO2016」烟花表演-LMLPHP

图 1

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

Loj #2568. 「APIO2016」烟花表演-LMLPHP

图 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\) 满足 \(1\le P_i<i\),代表和分叉点或烟花 \(i\) 相连的分叉点。\(C_i\) 代表连接它们的导火索长度 \((1\le C_i\le 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\times 10^5\)。

\(\\\)

参考博客

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

\[f_{fa_u}(x)=
\begin{cases}
f_u(x)+w & x\leq L\\
f_u(L)+w-(x-L) & L\leq x\leq L+w\\
f_u(L) & L+w <x\leq 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\)在\(x\geq x_1\)的部分斜率为\(1\),函数\(2\)在\(x\geq x_2\)的部分斜率为\(1\)(\(x_1<x_2\)),那么新函数在\(x_1\leq x\leq 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;
}
05-17 06:52