CSP2019 树的重心 题解

本题当然可以通过大力讨论每棵子树的size的大小关系,然后用各种数据结构暴力维护。但是我更倾向于用一种更为性质的做法。

首先讲一下我在考场上想到的做法(没写)。就是考虑换根,在换根的过程中计算每一条边删去后得到的两棵子树的重心, 由于重心的一些性质,如果我们把以点v的所有儿子为根的子树的重心求了出来,那么我们要求以点v为根的子树的重心时,可以保证重心一定在v的重儿子子树的重心到v的链上,那么我们就可以通过链上倍增来实现对多棵子树的重心的合并。在换根时维护每个点子树size,以及子树内的重心,换根时倍增求新子树的重心即可。

代码:

#include<bits/stdc++.h>
using namespace std;
#define N 300007
#define M 600007
#define ll long long
#define mem(x) memset(x,0,sizeof(x))
int hd[N],pre[M],to[M],num,wa[N],wb[N],sz[N],ts,dep[N];
int anc[N][23],ms[N],n;
ll ans;

void clear()
{
	mem(hd),mem(wa),mem(wb),mem(sz);
	mem(dep),mem(ms);
	num=0;ans=0;
}
	
int calc(int v)
{
	return max(ms[v],ts-sz[v]);
}
void solve(int x,int d,int &a,int &b)
{
	if(dep[x]<d)return;
	for(int i=20;i>=0;i--)
	{
		int mid=anc[x][i],y=anc[mid][0];
		if(dep[y]>=d&&calc(mid)>=calc(y))
			x=mid;
	}
	int y=anc[x][0];
	int dx=calc(x),dy=calc(y);
	if(dep[x]==d||dx<dy)
	{
		if(dx<=ts/2)a=x;
	}
	else
	{
		if(dy<=ts/2)
		{
			a=y;
			if(dx==dy)b=x;
		}
	}
}
/*void solve(int x,int d,int &a,int &b)
{
	while(dep[x]>=d)
	{
		if(calc(x)<=ts/2)
		{
			if(a)b=x;
			else a=x;
		}
		x=anc[x][0];
	}
}*/
int glca(int x,int y)
{
	if(dep[x]<dep[y])swap(x,y);
	for(int i=20;i>=0;i--)
		if(dep[x]-dep[y]>=(1<<i))
			x=anc[x][i];
	if(x==y)return x;
	for(int i=20;i>=0;i--)
		if(anc[x][i]!=anc[y][i])
			x=anc[x][i],y=anc[y][i];
	return anc[x][0];
}
int dmax(int x,int y)
{
	return dep[x]>dep[y]?x:y;
}
int dmin(int x,int y)
{
	return dep[x]<dep[y]?x:y;
}	
void dfs(int v,int f)
{
	dep[v]=dep[f]+1;
	anc[v][0]=f;
	for(int i=1;i<=20;i++)
		anc[v][i]=anc[anc[v][i-1]][i-1];
	sz[v]=1;
	int son=v;
	for(int i=hd[v];i;i=pre[i])
	{
		int u=to[i];
		if(u==f)continue;
		dfs(u,v);
		sz[v]+=sz[u];
		if(sz[u]>ms[v])ms[v]=sz[u],son=u;
	}
	ts=sz[v];
	int x=( son==v ? v : dmax(wa[son],wb[son]) );
	solve(x,dep[v],wa[v],wb[v]);
}
void dp(int v,int f)
{
	int s1,d1,s2,d2;
	s1=d1=s2=d2=0;
	for(int i=hd[v];i;i=pre[i])
	{
		int u=to[i];
		if(sz[u]>s1)s2=s1,d2=d1,s1=sz[u],d1=u;
		else if(sz[u]>s2)s2=sz[u],d2=u;
	}
	for(int i=hd[v];i;i=pre[i])
	{
		int u=to[i];
		if(u==f)continue;
		int x=d1;
		if(u==d1)x=d2;
		
		int _ms=ms[v],_wa=wa[v],_wb=wb[v];
		
		sz[v]-=sz[u],sz[u]+=sz[v];
		ms[v]=sz[x];
		wa[v]=wb[v]=0;
		ts=sz[v];
		
		int p=dmax(wa[x],wb[x]);
		//if(p==0)printf("WA
");
		if(x==f)
		{
			int q=dmin(wa[x],wb[x]);
			int lca=glca(p,v);
			if(q!=0&&q==anc[lca][0])p=q,lca=p;
			solve(p,dep[lca],wa[v],wb[v]);
			solve(v,dep[lca]+1,wb[v],wa[v]);
		}
		else
		{
			solve(p,dep[v],wa[v],wb[v]);
		}	
		
		/*if(!wa[v]&&!wb[v])
		{
			printf("%d
",v);
			printf("%d
",x);
		}*/
		ans+=wa[v]+wb[v]+wa[u]+wb[u];
 		dp(u,v);
		
		wa[v]=_wa,wb[v]=_wb;
		ms[v]=_ms;
		sz[u]-=sz[v],sz[v]+=sz[u];
	}
	
}
void adde(int x,int y)
{
	num++;pre[num]=hd[x];hd[x]=num;to[num]=y;
}
void dewa()
{
	for(int i=1;i<=n;i++)
		printf("%d: %d %d
",i,wa[i],wb[i]);
}

int main()
{
	//freopen("data.in","r",stdin);
	int t;
	scanf("%d",&t);
	while(t--)
	{
		clear();
		int x,y;
		scanf("%d",&n);
		for(int i=1;i<n;i++)
		{
			scanf("%d%d",&x,&y);
			adde(x,y),adde(y,x);
		}
		dfs(1,0);
		//dewa();
		dp(1,0);
		printf("%lld
",ans);
	}
	return 0;
}

还有一种题解做法,也是利用重心的性质,但是比我的更巧妙,而且实现也更简单。
就是考虑以某一个点v为根时怎么找它的重心,可以发现从点v出发,到重心的路径一定是不断沿着重儿子走,而每一个点的重儿子是唯一的,那么我们用倍增优化沿着重儿子走的过程,设f[i][j]为从点i开始,走(2^j)步走到的位置,我们只要找到最后一个满足(2*size[v]geq sum)的点即可,其中sum是割边后当前连通块的大小。同样像前一种做法一样换根即可,注意f[i][j]是可以换根维护的。

题解做法:

#include<bits/stdc++.h>
using namespace std;
#define N 300007
#define M 600007
#define ll long long
#define mem(x) memset(x,0,sizeof(x))
int hd[N],pre[M],to[M],num,son[N],fa[N],sz[N];
int ch[N][23],n;
ll ans;
void clear()
{
	mem(hd),mem(son);
	ans=0,num=0;
}
void adde(int x,int y)
{
	num++;pre[num]=hd[x];hd[x]=num;to[num]=y;
}
void mak_st(int v)
{
	for(int i=1;i<=20;i++)
		ch[v][i]=ch[ch[v][i-1]][i-1];
}
void dfs(int v,int f)
{
	fa[v]=f,sz[v]=1;
	for(int i=hd[v];i;i=pre[i])
	{
		int u=to[i];
		if(u==f)continue;
		dfs(u,v);
		sz[v]+=sz[u];
		if(sz[u]>sz[son[v]])son[v]=u;
	}
	ch[v][0]=son[v];
	mak_st(v);
}
void solve(int x,int ts)
{
	for(int i=20;i>=0;i--)
	{
		int y=ch[x][i];
		if(y&&2*sz[y]>=ts)x=y;
	}
	ans+=x;
	if(2*sz[x]==ts)ans+=fa[x];
}
void dp(int v,int f)
{
	int s1,s2;
	s1=s2=0;
	for(int i=hd[v];i;i=pre[i])
	{
		int u=to[i];
		if(sz[u]>sz[s1])s2=s1,s1=u;
		else if(sz[u]>sz[s2])s2=u;
	}
	for(int i=hd[v];i;i=pre[i])
	{
		int u=to[i];
		if(u==f)continue;
		int x=s1;
		if(u==s1)x=s2;
		
		sz[v]-=sz[u];
		ch[v][0]=x;
		mak_st(v);
		
		solve(v,sz[v]),solve(u,sz[u]);
		
		sz[u]+=sz[v],fa[v]=u;
		
		dp(u,v);
		
		sz[u]-=sz[v],sz[v]+=sz[u];
	}
	ch[v][0]=son[v];
	mak_st(v);
	fa[v]=f;
}
int main()
{
	//freopen("data.in","r",stdin);
	int t;
	scanf("%d",&t);
	while(t--)
	{
		clear();
		scanf("%d",&n); 
		int x,y;
		for(int i=1;i<n;i++)
		{
			scanf("%d%d",&x,&y);
			adde(x,y),adde(y,x);
		}
		dfs(1,0),dp(1,0);
		printf("%lld
",ans);
	}
	return 0;
}

比较两种做法,发现其实都利用的是重儿子的性质,只不过我的做法是从重儿子子树内往根走,而题解做法是从根往重儿子走。为什么题解做法比我的短了很多呢?
因为我的倍增数组是从下往上倍增,换根时根的改变会导致下面所有子孙结点的倍增数组改变,于是不能在换根过程中维护,所以我的倍增数组一直是以1号点为根的,就多了当前点的重儿子是从以1为根时的父亲方向来还是从儿子方向来的讨论。而题解是从上往下倍增的,换根时不会导致下面的点发生改变,而能够自上往下倍增的原因又是题解利用了每个点只会往唯一的重儿子走的性质,那么每个点出发走若干步走到的点是唯一的,就可以倍增了。

我在洛谷的题解中,看到了一篇大力讨论子树size的题解,里面的一些计数技巧还是有点意思的,比如说把计算每条边割断后子树重心标号之和转化为,对每一个点计算它会在多少条边被割断之后成为子树的重心,大家可以看一看。Mr_Wu 的博客

原文地址:https://www.cnblogs.com/lishuyu2003/p/11896697.html