[CSP-S 2019] 树的重心
Description
求一个树,对于所有边在单独删去的情况下,分裂出来的两个子树的重心的编号之和
\(7\leq N\leq 3\cdot 10^5\)
Solution
\(40\ pts\)
先看怎么拿满 \(AB\) 性质前的那些暴力分。
正好前几天了解过一个小结论,重心一定在整个树上的重链上。(即除根外全部都是重儿子)
知道了这个我们就可以枚举每个边断掉后对两个子树求重儿子然后找重心。
于是很轻松的能用 \(O(N^2)\) 的暴力拿到 \(40\) 分的好成绩。
接下来简单口胡一下小结论(会非常啰嗦,可以选择跳过,但绝对好懂):
我们从根开始,
\(1.\) 如果重儿子的子树大小没有原树大小的一半大了,同时其他儿子的子树大小不会更大,也不可能存在重心,所以此时根为重心。
注意此时只能有一个重心,因为根的重儿子的子树大小已经小于原树大小的一半了,如果重儿子为重心,重儿子下面的大小最少要减 \(1\)(如果他只有一个儿子时,大小只会减去他自己),加之本身就不到原树大小的一半,所以不可能存在第二个重心。
\(2.\) 如果重儿子的子树大小等于原树大小的一半,正好根为重心。
此时有可能根只有两个儿子并且大小一样,此时的话画个图就知道也只有根为中心。
另外的话有可能根的重儿子为重心二号,因为可能的是其他儿子子树和比原树大小的一半小(因为重儿子的子树大小占了一半,所以最多也只会小 \(1\) ),所以以重儿子为中心的话,除了重儿子以下的,剩下的点数就正好是子树的一半了,正好符合。
\(3.\) 如果重儿子的子树大小大于原树大小的一半,此时根不会是重心了。
然后就一直往重儿子去找,因为如果不这样,光是原来重儿子的子树就已经比目前搜到的轻儿子的子树大了,也不可能存在重心了。
终于叨完了。。
\(55/75\ pts\)
特殊分拿到手软!
手动模拟一下就能多拿到 \(15\) 分!
找找规律就能再拿 \(20\) 分!
\(100\ pts\)
主要拖累复杂度的还是枚举每条边。
考虑怎么不去枚举就能计算。
这种树上对于每个边而言计算答案的套路可能可以参考一些对点而言的题,可以上换根 \(\small DP\) 的思路
先还是以 \(1\) 为根找重儿子。
然后仍然是 \(dfs\) ,但每次不是仅对一个点计算,而是对于这条边上的两个点算(正好去掉这条边后,两个端点就可以算新树的根了)
然后怎么快速更新此时两个新树的信息呢?
(其实代码里面说的蛮清楚的我并不想再解释了。。只要配合着画个图就能快速理解了)
最后注意两下 \(chl\) 表示一个点只往重儿子那里向下跳 \(2^i\) 步。
然后是 \(son2\) 表示一个点的所有儿子中子树大小第二大的儿子(所以为什么叫二儿子嘛)
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=3e5+10;
int t,n,fst[N],tot,siz[N],sz[N],fth[N],ft[N];
ll ans;
int son[N],son2[N],sn[N],chl[N][20];
struct edge{
int nxt,to;
}e[N<<1];
struct mdzz{
int u,v;
}edg[N];
inline int read(){
int s=0,w=1;
char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+ch-'0';ch=getchar();}
return s*w;
}
inline void add(int u,int v){
e[++tot]=(edge){fst[u],v};
fst[u]=tot;
}
inline void dfs1(int u,int fa){
siz[u]=1;fth[u]=fa;
for(int i=fst[u];i;i=e[i].nxt){
int v=e[i].to;
if(v==fa)continue;
dfs1(v,u);
siz[u]+=siz[v];
if(siz[v]>siz[son[u]]){son2[u]=son[u];son[u]=v;}
else if(siz[v]>siz[son2[u]])son2[u]=v;
}
chl[u][0]=son[u];
for(int j=1;j<=17;++j)chl[u][j]=chl[chl[u][j-1]][j-1];
}
inline int sigg(int u,int sizz){
return (max(sz[sn[u]],sizz-sz[u])<=(sizz>>1))?1:0;
}
inline void dfs2(int u,int fa){
for(int i=fst[u];i;i=e[i].nxt){
int v=e[i].to,it;
if(v==fa)continue;
ft[u]=ft[v]=0;//断边
sz[u]=siz[1]-siz[v];
if(v==son[u])sn[u]=son2[u];
else sn[u]=son[u];
//切断的如果是重儿子就换二儿子
if(sz[fa]>sz[sn[u]])sn[u]=fa;
//此时u的fa也成了u的son之一,也判一下
chl[u][0]=sn[u];
for(int j=1;j<=17;++j)chl[u][j]=chl[chl[u][j-1]][j-1];
//重新算重儿子们
it=u;
for(int j=17;~j;--j)if(sz[chl[it][j]]>(sz[u]>>1))it=chl[it][j];
ans+=sn[it]*sigg(sn[it],sz[u])+it*sigg(it,sz[u])+ft[it]*sigg(ft[it],sz[u]);
it=v;
for(int j=17;~j;--j)if(sz[chl[it][j]]>(sz[v]>>1))it=chl[it][j];
ans+=sn[it]*sigg(sn[it],sz[v])+it*sigg(it,sz[v])+ft[it]*sigg(ft[it],sz[v]);
//结论之重心的父亲和重儿子有可能是第二个重心(但一共只能有两个重心,所以都判一下不会错)
ft[u]=v;
dfs2(v,u);
}
sz[u]=siz[u];ft[u]=fth[u];sn[u]=son[u];
chl[u][0]=son[u];
for(int j=1;j<=17;++j)chl[u][j]=chl[chl[u][j-1]][j-1];
//记得要回溯
}
inline void clear(){
tot=ans=0;
memset(fst,0,sizeof(fst));
memset(siz,0,sizeof(siz));
memset(fth,0,sizeof(fth));
memset(son,0,sizeof(son));
}
inline void mian(){
n=read();
clear();
for(int i=1;i<n;++i){
int u=read(),v=read();
edg[i]=(mdzz){u,v};
add(u,v);add(v,u);
}
dfs1(1,0);
memcpy(sz,siz,sizeof(sz));
memcpy(ft,fth,sizeof(ft));
memcpy(sn,son,sizeof(sn));
dfs2(1,0);
printf("%lld\n",ans);
}
int main(){
t=read();
for(int i=1;i<=t;++i)mian();
return 0;
}
最后,仔细想想的话,只要我们肯去思考,好像满分也不是那么难。
完结散花!