NOIP2019 树的重心
树的重心
小简单正在学习离散数学,今天的内容是图论基础,在课上他做了如下两条笔记:
-
一个大小为 \(n\) 的树由 \(n\) 个结点与 \(n − 1\) 条无向边构成,且满足任意两个结点间有且仅有一条简单路径。在树中删去一个结点及与它关联的边,树将分裂为若干个子树;而在树中删去一条边(保留关联结点,下同),树将分裂为恰好两个子树。
-
对于一个大小为 \(n\) 的树与任意一个树中结点 \(c\),称 \(c\) 是该树的重心当且仅当在树中删去 \(c\) 及与它关联的边后,分裂出的所有子树的大小均不超过 \(\lfloor \frac{n}{2} \rfloor\)(其中 \(\lfloor x \rfloor\) 是下取整函数)。对于包含至少一个结点的树,它的重心只可能有 \(1\) 或 \(2\) 个。
课后老师给出了一个大小为 \(n\) 的树 \(S\),树中结点从 \(1 \sim n\) 编号。小简单的课后作业是求出 \(S\) 单独删去每条边后,分裂出的两个子树的重心编号和之和。即:
上式中,\(E\) 表示树 \(S\) 的边集,\((u, v)\) 表示一条连接 \(u\) 号点和 \(v\) 号点的边。\(S'_u\) 与 \(S'_v\) 分别表示树 \(S\) 删去边 \((u, v)\) 后,\(u\) 号点与 \(v\) 号点所在的被分裂出的子树,\(c(S)\) 表示树 \(S\) 重心的集合。
小简单觉得作业并不简单,只好向你求助,请你教教他。
\(n\leq 3\times 10^5\)。
题解
根据期望的线性性(或者交换求和顺序),我们只需要求出每个点\(x\)成为了多少次重心即可。
先把\(x\)当做根,然后以删点\(y\)指代删掉\(y\)到父亲的边。如果要在儿子\(y\)子树删点,那么就要考虑\(y\)子树的大小和\(x\)除了\(y\)的其他儿子的子树大小,得出删掉的点数在某个区间\([l,r]\)内。那么DFS一遍即可得出答案。
显然我们不能枚举每个节点作为根。考虑以\(1\)为根,那么当\(y\)是\(x\)的儿子时,要求DFS序在某个区间内,并且子树大小属于\([l,r]\),变成了二维数点问题。当\(y\)是\(x\)的父亲时,如果删掉的点不是\(x\)的祖先,限制相同。当删掉的点是\(x\)的祖先时,情况会有不同,不过这个\(x\)到\(1\)的链的信息可以在DFS的时候维护下来。
时间复杂度\(O(n\log n)\)。
CO int N=3e5+10;
int n;
vector<int> to[N];
int pos[N],tim,idx[N];
int siz[N],lim[N][2];
void dfs(int x,int fa){
pos[x]=++tim,idx[tim]=x;
siz[x]=1,lim[x][0]=lim[x][1]=0;
for(int y:to[x])if(y!=fa){
dfs(y,x);
siz[x]+=siz[y];
if(siz[y]>lim[x][1]){
lim[x][1]=siz[y];
if(lim[x][1]>lim[x][0]) swap(lim[x][0],lim[x][1]);
}
}
if(n-siz[x]>lim[x][1]){
lim[x][1]=n-siz[x];
if(lim[x][1]>lim[x][0]) swap(lim[x][0],lim[x][1]);
}
}
namespace Seg{
int root[N],tot;
int lc[N*20],rc[N*20],sum[N*20];
#define mid ((l+r)>>1)
void insert(int&x,int l,int r,int p,int v){
++tot,lc[tot]=lc[x],rc[tot]=rc[x];
sum[tot]=sum[x]+v,x=tot;
if(l==r) return;
if(p<=mid) insert(lc[x],l,mid,p,v);
else insert(rc[x],mid+1,r,p,v);
}
int query(int x,int l,int r,int ql,int qr){
if(ql>qr or !x) return 0;
if(ql<=l and r<=qr) return sum[x];
if(qr<=mid) return query(lc[x],l,mid,ql,qr);
if(ql>mid) return query(rc[x],mid+1,r,ql,qr);
return query(lc[x],l,mid,ql,qr)+query(rc[x],mid+1,r,ql,qr);
}
#undef mid
}
namespace Bit1{
int sum[N];
#define lowbit(x) (x&-x)
void insert(int p,int v){
for(int i=p;i<=n;i+=lowbit(i)) sum[i]+=v;
}
int query(int p){
int ans=0;
for(int i=p;i;i-=lowbit(i)) ans+=sum[i];
return ans;
}
IN int query(int l,int r){
if(l>r) return 0;
return query(r)-query(l-1);
}
#undef lowbit
}
namespace Bit2{
int sum[N];
#define lowbit(x) (x&-x)
void insert(int p,int v){
for(int i=p;i<=n;i+=lowbit(i)) sum[i]+=v;
}
int query(int p){
int ans=0;
for(int i=p;i;i-=lowbit(i)) ans+=sum[i];
return ans;
}
IN int query(int l,int r){
if(l>r) return 0;
return query(r)-query(l-1);
}
#undef lowbit
}
int64 solve(int x,int fa){
int64 ans=0;
for(int y:to[x])if(y!=fa){
int L=max(2*siz[y]-n,1),R=n-2*(lim[x][0]==siz[y]?lim[x][1]:lim[x][0]);
ans+=Seg::query(Seg::root[pos[y]+siz[y]-1],1,tim,L,R)-Seg::query(Seg::root[pos[y]-1],1,tim,L,R);
}
int L=max(2*(n-siz[x])-n,1),R=n-2*(lim[x][0]==n-siz[x]?lim[x][1]:lim[x][0]);
ans+=Seg::query(Seg::root[pos[x]-1],1,tim,L,R);
ans+=Seg::query(Seg::root[tim],1,tim,L,R)-Seg::query(Seg::root[pos[x]+siz[x]-1],1,tim,L,R);
ans+=Bit2::query(L,R)-Bit1::query(L,R);
ans*=x;
Bit1::insert(siz[x],1);
for(int y:to[x])if(y!=fa){
Bit2::insert(n-siz[y],1);
ans+=solve(y,x);
Bit2::insert(n-siz[y],-1);
}
Bit1::insert(siz[x],-1);
return ans;
}
void real_main(){
read(n);
for(int i=1;i<=n;++i) to[i].clear();
for(int i=1;i<n;++i){
int x=read<int>(),y=read<int>();
to[x].push_back(y),to[y].push_back(x);
}
tim=0,dfs(1,0);
Seg::tot=0;
for(int i=1;i<=n;++i){
Seg::root[i]=Seg::root[i-1];
Seg::insert(Seg::root[i],1,n,siz[idx[i]],1);
}
printf("%lld\n",solve(1,0));
}
int main(){
freopen("centroid.in","r",stdin),freopen("centroid.out","w",stdout);
for(int T=read<int>();T--;) real_main();
return 0;
}