树的重心

一、什么是树的重心

所谓树的重心指的是删掉这个点之后可以使所有子树中大小最大的那一个最小。
树的重心满足一些性质:

  • 性质 \(1\):删掉树的重心之后,所有子树的大小不都超过 \(\lfloor\frac{n}{2}\rfloor\)\(n\) 指树的节点数量。
  • 性质 \(2\):树的重心最多有两个,如果有两个树的重心,那么这两个点一定是一条边的两个端点。
  • 性质 \(3\):树的重心到树上所有点的距离之和最小。

二、树的重心性质证明

1、性质 \(1\)

首先从性质 \(1\) 开始,首先要知道一个不需要证明的东西,就是如果连树的重心都无法满足性质 \(1\),那其它点也无法满足性质 \(1\),因为树的重心会使删掉这个点之后所有子树中大小最大的那一个最小,那么它肯定将所有子树分割的很均匀(学过贪心算法的人都知道吧),直白的讲就是把这些子树的大小分割的差不多,那么连这么分割都无法满足性质 \(1\),那其它点就更不可能满足性质 \(1\) 了,好了,然后继续,那么假设我们虚拟一个树,设置它的重心为 \(x\),如果 \(x\) 的子树中有一个的大小超过了 \(\lfloor\frac{n}{2}\rfloor\)(不可能有两个超过),其余的子树大小都小于 \(\lfloor\frac{n}{2}\rfloor\),那么我们可以把树的重心 \(x\) 跳到它这个儿子上,那么这个子树的大小自然就缩小了,其余子树的大小自然就放大了,那么这个时候,你会发现 \(x\) 的这个儿子比 \(x\) 更优,并且它的所有子树大小最多为 \(\lfloor\frac{n}{2}\rfloor\),这和 \(x\) 是树的重心矛盾,性质 \(1\) 得证。

2、性质 \(2\)

然后就是性质 \(2\),因为如果有多个树的重心,那么说明此时从任何一个重心调整到另外一个重心不影响答案,那么只有可能这个重心一边是极限(\(\lfloor\frac{n}{2}\rfloor\)),一边是极限减 \(1\)\(\lfloor\frac{n}{2}\rfloor-1\)),显然此时 \(n\) 一定是偶数,否则加在一起答案就不是 \(n\),然后由于调整都是调整到儿子上,所以从这个重心跳到另外一个重心,它只需要经过一条边,那么此时重心就不可能有大于两个了(只有这个重心和它的儿子可以互相调整而不影响答案),而且都是在一条边上,性质 \(2\) 得证。

3、性质 \(3\)

这个性质的证明就很有趣了,我们先放一个程序:

void dfs(int x,int fa)
{
	s[x] = 1;
	for*(int i = 0;i<a[x].size();i++)
	{
		int v = a[x][i];
		if(v!=fa)
		{
			dfs(v,x);
			s[x]+=s[v];
			d[x]+=d[v]+s[v];
		}
	}
}
dfs(1,0);
ans[1] = d[1];
void dfs1(int x,int fa)
{
	for*(int i = 0;i<a[x].size();i++)
	{
		int v = a[x][i];
		if(v!=fa)
		{
			dfs(v,x);
			ans[v] = ans[x]-s[v]+n-s[v];
		}
	}
}
dfs1(1,0);

这个程序中的 \(ans\) 数组表示从这个点到所有点的距离之和,如果看不懂的话请学习换根 dp
首先我们需要考虑一个问题,如何使 \(ans_v<ans_x\)?其实就是要使 \(n-2 \times s_v>0\),移项后得 \(s_v>\lfloor\frac{n}{2}\rfloor\),这个时候你就会发现这个找最小的 \(ans\) 的过程和不断调整树的重心的过程是一样的,就是当 \(s_v>\lfloor\frac{n}{2}\rfloor\) 的时候,让 \(v\) 当重心一定比让 \(x\) 当重心更优,然后如果 \(s_v>\lfloor\frac{n}{2}\rfloor\)\(ans_v<ans_x\),所以说树的重心的 \(ans\) 一定是最小的,性质 \(3\) 得证。

三、树的重心求法

树的重心有很多种求法,这里只说 dfs 做法。
就是先 dfs 求每个子树大小,在 dfs 的过程中计算一下如果删去当前节点 \(x\),所有子树中最大的大小是多少,然后取个 \(\min\) 就行了,当然,找最大的子树时一定别忘了 \(x\) 的父亲那边的子树。
代码:

int minn = 1e9,minnn = 0;
void dfs(int x,int fa)
{
	s[x] = 1;//初始化
	int maxx = 0;//找最大子树的变量
	for(int v:e[x])
	{
		if(v!=fa)
		{
			dfs(v,x);
			s[x]+=s[v];//加上儿子子树的大小
			maxx = max(maxx,s[v]);//找到最大的子树
		}
	}
	maxx = max(maxx,n-s[x]);//别忘了还有父亲
	if(maxx<minn)//如果比当前的最小值还要小
	{
		minn = maxx;//记录最小值
		minnn = x;//记录重心
	}
}

四、例题讲解

CF685B Kay and Snowflake

此题就是应用树的重心的性质 \(1\) 来求解的,首先我们根据树的重心调整原则,可以知道只有树的重心才满足删除后最大子树的大小不超过 \(\lfloor\frac{n}{2}\rfloor\)(很简单的原理),那么我们其实只需要做一遍 dfs 求子树大小,顺便计算当 \(x\) 的子树全部都小于等于 \(\lfloor\frac{n}{2}\rfloor\) 时,那么以 \(x\) 为根的子树的重心就是 \(x\),否则就进行调整,调整到那个子树大小超过 \(\lfloor\frac{n}{2}\rfloor\) 的那个儿子,然后如果还不满足的话,就再往上跳若干步(毕竟不可能往下跳,只能往上跳)。
搜索代码:

void dfs(int x)
{
	s[x] = 1;//初始化
	for(int i = 0;i<a[x].size();i++)
	{
		int v = a[x][i];
		dfs(v);
		s[x]+=s[v];//加上儿子这个子树的大小
	}
	ans[x] = x;//ans[x]表示x这个子树的重心,这里初始化
	for(int i = 0;i<a[x].size();i++)
	{
		int v = a[x][i];
		if(s[v]*2>s[x])//如果儿子这个子树大于s[x]/2
		{
			ans[x] = ans[v];//先赋值成ans[v]
			while((s[x]-s[ans[x]])*2>s[x])//由于多了一个点x,所以可能导致v这个子树的重心不是x这个子树的中心,所以要往上跳,直到跳到了x这个子树的重心
			{
				ans[x] = fa[ans[x]];//跳!
			}
		}
	}
}

时间复杂度?由于它只会跳一次,因为不可能有两个 \(x\) 的儿子都满足子树大小大于 \(\lfloor\frac{n}{2}\rfloor\),因为这样就大于 \(n\) 了,而且每一次最多跳一步,所以时间复杂度为 \(O(n)\)
附上总代码:

#include<bits/stdc++.h>
using namespace std;
const int N = 3e5+5;
vector<int>a[N];
int s[N],ans[N],fa[N];
void dfs(int x)
{
	s[x] = 1;
	for(int i = 0;i<a[x].size();i++)
	{
		int v = a[x][i];
		dfs(v);
		s[x]+=s[v];
	}
	ans[x] = x;
	for(int i = 0;i<a[x].size();i++)
	{
		int v = a[x][i];
		if(s[v]*2>s[x])
		{
			ans[x] = ans[v];
			while((s[x]-s[ans[x]])*2>s[x])
			{
				ans[x] = fa[ans[x]];
			}
		}
	}
} 
signed main()
{
	int n,q;
	scanf("%d %d",&n,&q);
	for(int i = 2;i<=n;i++)
	{
		scanf("%d",&fa[i]);
		a[fa[i]].push_back(i);
	}
	dfs(1);
	while(q--)
	{
		int x;
		scanf("%d",&x);
		printf("%d\n",ans[x]);
	}
    return 0;
}
posted @ 2025-01-22 23:13  林晋堃  阅读(141)  评论(0)    收藏  举报