专题 树的重心
概念解释
定义
树的重心是指对于一颗无根树,找到一个结点C,使得把树变成以C点为根的有根树时,最大子树的结点数最小。
性质
-
树的重心如果不唯一,则至多有两个,且这两个重心相邻。
-
以树的重心为根时,所有子树的大小都不超过整棵树大小的一半。
-
树中所有点到某个点的距离和中,到重心的距离和是最小的;如果有两个重心,那么到它们的距离和一样。
-
把两棵树通过一条边相连得到一棵新的树,那么新的树的重心在连接原来两棵树的重心的路径上。
-
在一棵树上添加或删除一个叶子,那么它的重心最多只移动一条边的距离。
我们考虑对以上性质作一个简单的证明。
树的重心性质完整证明
树的重心是树形结构中的重要概念,在算法设计和图论中有广泛应用。本文将严谨证明树重心的五个核心性质。
重心不唯一时,至多有两个且相邻
证明
设树有节点数 n,重心定义为删除该点后,剩余最大连通块(子树)的大小最小。
假设存在两个不相邻的重心 u 和 v,路径 u → v 上至少有一个中间节点 w。
删除 u:剩余最大连通块大小 ≤ ⌊n/2⌋。
删除 v:剩余最大连通块大小 ≤ ⌊n/2⌋。
u — w — v (不相邻假设)
考虑断开 u 和 v 之间的边,树被分为两部分 T_u(含 u)和 T_v(含 v)。
对 u,包含 T_v 的连通块大小为 |T_v|。
对 v,包含 T_u 的连通块大小为 |T_u|。
由重心性质:
但 |T_u| + |T_v| = n,因此:
等式成立仅当 |T_u| = |T_v| = n/2(n 为偶数)。
此时路径 u → v 上必有节点 w,删除 w 后:
若 w 在 T_u 侧,包含 T_v 的连通块大小为 n/2。
但 T_u 内部被分割,其中一个连通块大小 < n/2(因为 w ≠ u),这与重心的性质矛盾。
以重心为根时子树大小不超过一半
证明
设重心为 c,以 c 为根,子树为 T_1, T_2, \dots, T_k。
c (根节点)
├─ T₁
├─ T₂
└─ ...
删除 c 后,剩余连通块即为各子树 T_i。
由重心定义,删除重心后得到的最大连通块大小不超过 ⌊n/2⌋:
因此,当以重心为根时,所有子树的大小都不超过 ⌊n/2⌋。
重心最小化距离和
证明
定义距离和函数 S(u) = \sum_{v \in V} \text{dist}(u, v)。
考虑从节点 u 移动到相邻节点 w,设 w 所在子树大小为 s:
分析:
- 若 s < n/2,则 S(w) > S(u),说明 w 不是更优点
- 若 s > n/2,则 S(w) < S(u),即 u 不是重心
因此,u 是重心当且仅当所有邻接子树大小 ≤ n/2,此时 S(u) 最小。
当存在相邻的两个重心 c_1 和 c_2 时:
c₁ — c₂ (相邻重心)
两边子树大小均为 n/2
连接两棵树,重心在原重心路径上
证明
这个证明比较直观。考虑极限的思想。我们关注重心的物理意义,发现它会使整个系统变得”平衡“(稳定)。所以新的重心要使新树稳定,必然要使最大子树最小,这是毫无疑问的。所以新重心必在路径c1→c2 上。假设存在重心 u∉,设 (对称)。删除 :最大连通块为以下之一: 中不含 的部分(大小 );(大小 当 )矛盾(因重心要求最大块 )。
添加/删除叶子,重心移动至多一条边
理解:局部修改对重心位置影响有限。
证明(以添加叶子为例):
- 设原树重心 ,添加叶子 连向 ,新树大小 。
- 若 的子树仍满足 ,则重心不变。
- 否则,最大子树大小 ,记为 。
- max 必包含新叶子 (否则原 非重心)。
- 重心必在 的路径上(由性质4推论)。
- 移动至多沿路径一步:若 不满足,则需移动到 的方向,但不会跳过u(因路径上连续检查)。
算法分析
我们首先对这个概念作一个比较细致的研讨。下面列举了几个树的重心常见的应用,我们分成三个板块来讲。事实上,每个板块都是有两个方法,这也是这类问题常用的一些解题套路:DFS和树型DP。后者可能会陌生,但是前者一定熟悉。所以,以下我们以这几个例子,着重介绍树型DP的具体实操方法和注意到细节。
1.求树的重心
一个直观的想法就是按照定义去暴力DFS。每个节点都考虑一遍,最坏是O(n2)的时间复杂度。这不能接受。定义数组maxson[u],意义是根为u的有根树中最大子树的结点数,那么有: maxson[u]=max{siz(vi),n-siz(u)} vi为u的儿子结点,n为树的结点总数。 最后扫描一遍maxson[]数组,就可以得到树的重心了。这样是DFS统计的方法。
2.求树中所有点到重心的距离之和
现在求这个。还是从暴力开始思考起。无非是DFS枚举每一个点,统计答案。事实上,做了很多重复的工作,所以是有优化的余地的。考虑两遍DFS。又有两个方法:自底向上回溯时统计答案,或者自顶向下递推求解。先介绍前者。
首先要定义两个数组:size[u]和f[u]。分别表示以 u 为根节点的子树内节点个数,以及树中所有结点到结点 u 的距离之和。紧接着,考虑DFS,在递归返回即回溯之时,将子节点的答案传给父亲。考虑f[]数组怎样计算?关注定义,由于是带权图(即使不带,默认为1),用组合计数的原理,一共有size[u]个节点,每个节点到父节点都要经过 w(u,v) 这条边,也就是会增加 size[u]*w(u,v) 的边权。这样就方便转移了。
我们再介绍一下树型DP的做法。dfs1同方法1的dfs2求出到某个点的距离之和。dfs2换根法递推求到重心的距离之和。也就是先预处理一遍,然后钦定某个节点为根,计算这个根的信息。
f[i]=f[fa[i]]+(siz[root]-2*siz[i])*w(i,fa[i])
这个递推式是树形DP中计算重心转移时距离和的核心公式,其推导过程如下:
1. 前置定义
- f[u]:以u为重心时,所有节点到u的距离和
- size[v]:以v为根的子树节点总数
- size[1]:整棵树的节点总数(通常根节点为1)
- w(u,v):边(u,v)的权值(代码中的
e[i].w)
2. 推导逻辑
当重心从父节点u移动到子节点v时,距离和的变化分为三部分:
(1)v子树内的节点(共size[v]个)
- 原距离:
dist(x,u) = dist(x,v) + w(u,v) - 新距离:
dist(x,v) - 距离减少:
w(u,v) - 总减少量:
size[v] * w(u,v)
(2)u的其他子树节点(共size[u] - size[v]个)
- 原距离:
dist(x,u) - 新距离:
dist(x,u) + w(u,v) - 距离增加:
w(u,v) - 总增加量:
(size[u] - size[v]) * w(u,v)
(3)u的父方向子树节点(共size[1] - size[u]个)
- 原距离:
dist(x,u) - 新距离:
dist(x,u) + w(u,v) - 距离增加:
w(u,v) - 总增加量:
(size[1] - size[u]) * w(u,v)
(4) 合并所有的变化量:
4. 关键点说明
-
size[1]的作用:代表整棵树的节点总数,用于计算父方向子树的节点数。 - 为什么是
2*size[v]:-size[v](v子树的减少)和-size[v](u其他子树的增加中抵消的size[v])合并而来。 - 时间复杂度:通过两次DFS实现O(N)计算所有节点的f[v]。
(5). 示例验证
以如下树为例(边权=1,size[1]=5):
1(u) / \ 2(v) 3 / \ 4 5
初始f[u]=6(0+1+1+2+2)
计算f[v]:
f[v] = 6 + (5 - 2*3)*1 = 5
手动验证:1(1→2) + 0(2→2) + 2(3→2) + 1(4→2) + 1(5→2) = 5 ✔️
该公式的物理意义是:当子树v的节点数∑超过总数一半时(2*size[v] > size[1]),转移会使总距离减小,此时v更可能是重心。
3.树中任意两点距离的总和
对于一条边(如 a-b边)来说,端点a和b将这个树划分为两个部分,a部分子树上的点如果要连接到b部分,必定要经过a-b这条边,反过来也是。所以树上每条边对距离总和的贡献为两段经过次数的乘积再乘以边的权值。这就可以导出一个式子:
s[u]+=s[v]+f[v]∗siz[u]+f[u]∗siz[v]+siz[v]∗siz[u]∗w,
s[u]的意义是u子树中任意两点距离的总和; f[u]的意义是u子树中所有点到u距离的总和;
证明从略。
参考程序
本题是求重心的例题。
#include<iostream>
#include<cstdio>
using namespace std;
const int N=2e5+5;
struct edge{int next,to;}e[N<<1];
int head[N],idx;
int n,ans=N;
int size[N],dp[N];
inline void add(int u,int v){e[++idx]={head[u],v};head[u]=idx;}
void dfs(int u,int fa)
{
size[u]=1;
for(int i=head[u];i;i=e[i].next)
{
int v=e[i].to;
if(v!=fa)
{
dfs(v,u);
size[u]+=size[v];
dp[u]=max(dp[u],size[u]);
}
}
dp[u]=max(dp[u],n-size[u]);
ans=min(ans,dp[u]);
}
int main()
{
scanf("%d",&n);
for(int i=1;i<n;i++)
{
int u,v;
scanf("%d%d",&u,&v);
add(u,v),add(v,u);
}
dfs(1,0);
printf("%d\n",ans);
return 0;
}
本题是求树中所有点到重心的距离之和的例题。
方法一:
#include<iostream>
#include<cstdio>
using namespace std;
const int N=4e5+5;
struct edge{int next,to,w;}e[N];
int head[N],idx;
int n,ans,dp[N],size[N],Barycenter,f[N];
inline void add(int u,int v,int w){e[++idx]={head[u],v,w};head[u]=idx;}
void dfs1(int u,int fa)
{
size[u]=1;
for(int i=head[u];i;i=e[i].next)
{
int v=e[i].to;
if(v!=fa)
{
dfs1(v,u);
size[u]+=size[v];
dp[u]=max(dp[u],size[v]);
}
}
dp[u]=max(dp[u],n-size[u]);
if(dp[u]<=n>>1) Barycenter=u;
}
void dfs2(int u,int fa)
{
size[u]=1;
for(int i=head[u];i;i=e[i].next)
{
int v=e[i].to;
if(v!=fa)
{
dfs2(v,u);
size[u]+=size[v];
f[u]+=f[v]+size[v]*e[i].w;
}
}
}
int main()
{
scanf("%d",&n);
for(int i=1;i<n;i++)
{
int u,v,w; scanf("%d%d%d",&u,&v,&w);
add(u,v,w),add(v,u,w);
}
dfs1(1,0);
dfs2(Barycenter,0);
printf("%d %d\n",Barycenter,f[Barycenter]);
return 0;
}
方法二:
#include<iostream>
#include<cstdio>
using namespace std;
const int N=4e5+5;
struct edge{int next,to,w;}e[N];
int head[N],idx;
int n,ans,dp[N],size[N],Barycenter,f[N];
int cnt=0x3f3f3f3f;
inline void add(int u,int v,int w){e[++idx]={head[u],v,w};head[u]=idx;}
void dfs1(int u,int fa)
{
size[u]=1;
for(int i=head[u];i;i=e[i].next)
{
int v=e[i].to;
if(v!=fa)
{
dfs1(v,u);
size[u]+=size[v];
//dp[u]=max(dp[u],size[v]);
f[u]+=f[v]+size[v]*e[i].w;
}
}
//dp[u]=max(dp[u],n-size[u]);//dp[]找出最大的子树
//if(dp[u]<=n>>1) Barycenter=u;
}
void dfs2(int u,int fa)
{
//size[u]=1;
if(f[u]<cnt) cnt=f[u],Barycenter=u;
for(int i=head[u];i;i=e[i].next)
{
int v=e[i].to;
if(v!=fa)
{
f[v]=f[u]+(size[1]-size[v]*2)*e[i].w;
dfs2(v,u);
//size[u]+=size[v];
//f[u]+=f[v]+size[v]*e[i].w;
}
}
}
int main()
{
scanf("%d",&n);
for(int i=1;i<n;i++)
{
int u,v,w; scanf("%d%d%d",&u,&v,&w);
add(u,v,w),add(v,u,w);
}
/*dfs1(1,0);
dfs2(Barycenter,0);
printf("%d %d\n",Barycenter,f[Barycenter]);*/
dfs1(1,0);
dfs2(1,0);
cout<<Barycenter<<' '<<f[Barycenter];
return 0;
}
本题是树中任意两点距离的总和的例题。
方法一:
#include<iostream>
#include<cstdio>
using namespace std;
using ll=long long;
const int N=4e5+5;
struct edge{int next,to,w;}e[N];
int head[N],idx;
ll size[N],s[N];
int n;
inline void add(int u,int v,int w){e[++idx]={head[u],v,w};head[u]=idx;}
void dfs(int u,int fa)
{
size[u]=1;
for(int i=head[u];i;i=e[i].next)
{
int v=e[i].to,w=e[i].w;
if(v!=fa)
{
dfs(v,u);
s[u]+=s[v]+(ll)(n-size[v])*size[v]*w;
size[u]+=size[v];
}
}
//ans+=s[u];
}
int main()
{
scanf("%d",&n);
for(int i=1;i<n;i++)
{
int u,v,w; scanf("%d%d%d",&u,&v,&w);
add(u,v,w),add(v,u,w);
}
dfs(1,0);
printf("%lld\n",s[1]);
return 0;
}
方法二:
#include<iostream>
#include<cstdio>
using namespace std;
using ll=long long;
const int N=4e5+5;
struct edge{int next,to,w;}e[N];
int head[N],idx;
ll size[N],s[N],f[N];
int n;
inline void add(int u,int v,int w){e[++idx]={head[u],v,w};head[u]=idx;}
void dfs(int u,int fa)
{
size[u]=1;
for(int i=head[u];i;i=e[i].next)
{
int v=e[i].to,w=e[i].w;
if(v!=fa)
{
dfs(v,u);
s[u]+=s[v]+f[v]*size[u]+f[u]*size[v]+(ll)size[u]*size[v]*w;
f[u]+=f[v]+(ll)size[v]*w;
size[u]+=size[v];
//s[u]+=s[v]+(ll)(n-size[v])*size[v]*w;
//size[u]+=size[v];
}
}
//ans+=s[u];
}
int main()
{
scanf("%d",&n);
for(int i=1;i<n;i++)
{
int u,v,w; scanf("%d%d%d",&u,&v,&w);
add(u,v,w),add(v,u,w);
}
dfs(1,0);
printf("%lld\n",s[1]);
return 0;
}
细节研讨
还好,细节不多,这里就不展开了。
总结归纳
这里主要是是了解树重心的一些性质,掌握这证明的技巧与方法,最后来了解一下应用。但是最核心的是树型DP,这个以后会具体展开座作一研究的。

浙公网安备 33010602011771号