树的重心‘s 题解

题目来自2019年csp-s D2T3。

两个字诠释,毒瘤。。。
题目链接(来自某谷)
简单介绍下题面:
对于每组数据会给出一棵树,每次割去一条边分成两个棵树后,把这两棵树的重心的编号进行相加(用到一个题目给出的定理,每棵树的重心显然只会有一个或者两个),问你所有情况的结果累加的和(及把树上每条边割去后的结果累加)。

先介绍一下40分的纯暴力:

不妨假设以 1 1 1 为根节点,我们可以预处理出以每个节点为根的子树的大小 s i z [ i ] siz[i] siz[i] 并记录每个点的父亲,然后枚举每条边,把这条边做一个删除标记,显然这条边是连接一个节点 x x x 和它的父节点的,然后就对分成的两棵树进行分类操作。
显然以 x x x 为根节点的子树中每个点的 s i z [ i ] siz[i] siz[i] 是不会改变的,那么只要一遍dfs求出哪些点的 s i z [ i ] siz[i] siz[i] s i z [ x ] − s i z [ i ] siz[x]-siz[i] siz[x]siz[i] 都是 < = s i z [ x ] / 2 <=siz[x]/2 <=siz[x]/2 的,那么这个点就是一个重心,累加到答案上。
然后另外一个部分我们只要一遍操作从 x x x 开始,每次找到当前点的父亲并减去 s i z [ x ] siz[x] siz[x] 直到当前点的父亲为 0 0 0 ,那么停止操作,然后再在这棵树上跑一边如上的dfs,跑完之后再把减去的大小加回来即可。
主要代码:

void sdfs(int x,int fa){
	siz[x]=1;
	f[x]=fa;
	for(int i=top[x],y;i;i=e[i].x){
		y=e[i].y;
		if(y==fa) continue ;
		sdfs(y,x);
		siz[x]+=siz[y];
	}
}
void zdfs(int x,int fa,int rot){
	if(siz[rot]-siz[x]>siz[rot]/2) fl[x]=1;
	if(siz[x]>siz[rot]/2) fl[f[x]]=1;
	for(int i=top[x],y;i;i=e[i].x){
		y=e[i].y;
		if(y==fa) continue ;
		zdfs(y,x,rot);
	}
}


sdfs(1,0);
for(int i=0;(i+=2)<=ans;){
	memset(fl,0,sizeof(fl));
	if(f[u[i/2]]==v[i/2]){
		for(int j=v[i/2];j;j=f[j])
			siz[j]-=siz[u[i/2]];
		f[u[i/2]]=0;
		zdfs(v[i/2],u[i/2],1);
		zdfs(u[i/2],v[i/2],u[i/2]);
		for(int j=0;++j<=n;fl[j]?:sum+=j);
		for(int j=v[i/2];j;j=f[j])
			siz[j]+=siz[u[i/2]];
		f[u[i/2]]=v[i/2];
	}
	else{
		for(int j=u[i/2];j;j=f[j])
			siz[j]-=siz[v[i/2]];
		f[v[i/2]]=0;
		zdfs(u[i/2],v[i/2],1);
		zdfs(v[i/2],u[i/2],v[i/2]);
		for(int j=0;++j<=n;fl[j]?:sum+=j);
		for(int j=u[i/2];j;j=f[j])
			siz[j]+=siz[v[i/2]];
		f[v[i/2]]=u[i/2];
	}
}
再来考虑一下满足特殊性质A的15分(树的形态为一条链)

很可惜这15分在考场上写挂了。。。
首先模拟割几个边试试。
然后你就会发现,一条链上开始的点、末尾的点以及最中间的点(题目中所给条件是点数为奇数,当然如果不是奇数那就是最中间的两个点,但在代码中我没体现)被统计了两次,另外的都被统计了3次,所以只要一遍dfs求深度(为减少代码量可以用上面的求以它为根节点的子树大小,效果一样),然后再一个 O ( n ) O(n) O(n) 的for循环判断累加答案即可。
注意题目并未说链是1~n的,所以我们需要找入度为1的点进行dfs。
所以开个 d i n din din 来记录度数找开始的点。
主要代码:

for(int i=0;++i<=n;)
	if(din[i]==1){
		sdfs(i,0);
		break ;
	}
for(int i=0;++i<=n;siz[i]==1||siz[i]==n||siz[i]==n/2+1?sum+=i*2:sum+=i*3);
接下来满足特殊性质B的20分(树的形态为一棵完美二叉树)

考场上没推出来,考试结束想出来了结果第一发没开long long炸了。。。
照样,模拟割几个边试试。
然后你就又会惊奇的发现,根节点会在割去最后一层的边的时候被统计,总共会被统计 ( n + 1 ) > > 1 (n+1)>>1 (n+1)>>1 次(按题目所给的就是 1 < < 17 1<<17 1<<17 次),而根节点的子节点会在割去另外一个以子节点为根的子树上的边以及这两个子节点与根节点的连边时被统计,总共会被统计 ( ( n − 1 ) > > 1 ) + 1 ((n-1)>>1)+1 ((n1)>>1)+1 次(按题目所给的也是 1 < < 17 1<<17 1<<17 次),另外的点只会在割去它们与它们的父节点的连边时被统计一次,所以只要这样累加到答案上即可。
同样,注意它不是有序的二叉树。
同样开 d i n din din 来找。
主要代码:

void rcz(int x){
	sum+=1LL*n*(n+1)/2;
	sum+=1LL*x*((1<<17)-1);
	for(int i=top[x],y;i;i=e[i].x){
		y=e[i].y;
		sum+=1LL*y*((1<<17)-1);
	}
}


for(int i=0;++i<=n;)
	if(din[i]==2){
		rcz(i);
		break ;
	}
终于是可以写正解了。。。

先给个令人放松的预告:这里是两个dfs+一个倍增过的。
显然算法不难(虽然我在考场上没想出来,而且我这份代码有2KB多,我自己还调了3个小时才调出来。。。
思路其实就是在暴力上做了个优化差不多。。。
显然,我们可以发现,对于一棵树,从一个点开始找,若当前节点非重心,那么可以再去找它的重儿子和父节点是否为重心。
首先在第一个dfs中你要顺便求出以每个节点的最重和次重的儿子 g [ 1 / 2 ] [ x ] g[1/2][x] g[1/2][x](求次重是因为最重的儿子可能会被割掉)。
然后用 p [ i ] [ x ] p[i][x] p[i][x] 表示从x开始沿重儿子向下方跳 2 i 2^i 2i 步。
第二次dfs换根,把以1为根改为以 x x x 为根,然后枚举和它相连的每条没被扫过的边,再去找两边的重心,显然如果是下方节点则可以直接跳,若不是则在换根的时候记录一下,倍增的方法一样。
然后,你就AC了这题。。。
主要代码
算了直接贴个完整的code吧:

#include <bits/stdc++.h>
using namespace std;
const int N=3e5+10;
int t,n,ans,top[N],siz[2][N],g[3][N],f[2][N],p[20][N];
long long sum;
struct lol {int x,y;} e[N<<1];
void freo(){
	freopen("centroid.in","r",stdin);
	freopen("centroid.out","w",stdout);
}
void prep(){
	scanf("%d",&t);
}
void ein(int x,int y){
	e[++ans]=(lol){top[x],y};
	top[x]=ans;
}
void init(){
	scanf("%d",&n);
	sum=ans=0;
	memset(top,0,sizeof(top));
	memset(g,0,sizeof(g));
	memset(f,0,sizeof(f));
	for(int i=0,u,v;++i<n;scanf("%d%d",&u,&v),ein(u,v),ein(v,u));
}
void gdfs(int x,int fa){
	siz[1][x]=1;
	f[1][x]=fa;
	for(int i=top[x],y;i;i=e[i].x){
		y=e[i].y;
		if(y==fa) continue ;
		gdfs(y,x);
		siz[1][x]+=siz[1][y];
		siz[1][y]<=siz[1][g[1][x]]?
			siz[1][y]<=siz[1][g[2][x]]?:
				g[2][x]=y:
			(g[2][x]=g[1][x],g[1][x]=y);
	}
	p[0][x]=g[1][x];
	for(int i=0;++i<=18;p[i][x]=p[i-1][p[i-1][x]]);
}
int gg(int x,int tmp){
	return x*(max(siz[0][g[0][x]],tmp-siz[0][x])<=tmp/2);
}
void hdfs(int x,int fa){
	for(int i=top[x],y,cnt;i;i=e[i].x){
		y=e[i].y;
		if(y==fa) continue ;
		siz[0][x]=siz[1][1]-siz[1][y];
		f[0][y]=f[0][x]=0;
		g[0][x]=(g[1][x]==y?g[2][x]:g[1][x]);
		siz[0][fa]<=siz[0][g[0][x]]?:g[0][x]=fa;
		p[0][x]=g[0][x];
		for(int j=0;++j<=18;p[j][x]=p[j-1][p[j-1][x]]);
		cnt=x;
		for(int j=19;--j>=0;
			siz[0][x]-siz[0][p[j][cnt]]>siz[0][x]/2?:cnt=p[j][cnt]);
		sum+=gg(g[0][cnt],siz[0][x])+gg(cnt,siz[0][x])+gg(f[0][cnt],siz[0][x]);
		cnt=y;
		for(int j=19;--j>=0;
			siz[0][y]-siz[0][p[j][cnt]]>siz[0][y]/2?:cnt=p[j][cnt]);
		sum+=gg(g[0][cnt],siz[0][y])+gg(cnt,siz[0][y])+gg(f[0][cnt],siz[0][y]);
		f[0][x]=y;
		hdfs(y,x);
	}
	g[0][x]=p[0][x]=g[1][x];
	f[0][x]=f[1][x];
	for(int i=0;++i<=18;p[i][x]=p[i-1][p[i-1][x]]);
	siz[0][x]=siz[1][x];
}
void work(){
	gdfs(1,0);
	memcpy(siz[0],siz[1],sizeof(siz[0]));
	memcpy(g[0],g[1],sizeof(g[0]));
	memcpy(f[0],f[1],sizeof(f[0]));
	hdfs(1,0);
}
void prin(){
	printf("%lld\n",sum);
}
int main(){
	freo();
	for(prep();t--;init(),work(),prin());
	return 0;
}

那么这题就结束了。

感谢各位大佬捧场!(orz)
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值