【树的重心】
如果在树中选择某个节点并删除,这棵树将分为若干棵子树 ,**统计子树节点数并记录最大值。**取遍树上所有节点,使此最大值取到最小的节点被称为整个树的重心。
这里以及下文中的子树若无特殊说明都是指无根树的子树,即包括向上的那棵子树,并且不包括整棵树自身。如果题目明确说明存在点权,那么统计子树的节点数变成统计子树的权值之和。

【解法 - 树形 dp】
- 根据定义,利用树形 dp 求解:对于每一个结点,求出所有子树大小的最大值,找出最大值最小的结点即可。
以 x 为根的子树为例,除了所有的孩子结点 y 为根的子树以外,还有一棵向上的子树,这棵子树的大小可以用总的结点个数 n 减去以 x 为根的子树大小求得。因此:
- 状态表示:fx 表示以 x 为根的子树的大小;
- 状态转移方程:fx = 1 + sum (fy);
- 可以用全局变量来记录树的重心。
【性质】
- 树的重心如果不唯一,则至多有两个,且这两个重心相邻。
- 以树的重心为根时,所有子树的大小都不超过整棵树大小的一半。
- 树中所有点到某个点的距离和 中,到重心的距离和是最小的;如果有两个重心,那么到它们的距离和一样。
- 推论一:即使边权不为 1,只要没有负边权,不影响各点到重心的距离和最小;
- 推论二:即使边权和点权都不为 1,只要没有负边权,不影响各点到重心的距离和最小。
【模板题】
U104609 【模板】树的重心 - 洛谷
https://www.luogu.com.cn/problem/U104609代码:
cpp
#include <bits/stdc++.h>
using namespace std;
const int N = 5e4 + 10;
int n;
vector<int> edges[N];
int r1, r2; // 最多有两个重心
int ret = N; // 记录所有最大子树的最小值
int f[N]; // 以x为根的子树的大小
void dfs(int x, int fa)
{
f[x] = 1;
int m = 0;
for(auto y : edges[x])
{
if(y == fa) continue;
dfs(y, x);
m = max(m, f[y]);
f[x] += f[y];
}
m = max(m, n - f[x]);
if(m < ret)
{
r1 = x;
r2 = 0;
ret = m;
}
else if(m == ret)
{
r2 = x;
}
}
signed main()
{
cin >> n;
for(int i = 1; i < n; i++)
{
int u, v; cin >> u >> v;
edges[u].push_back(v);
edges[v].push_back(u);
}
dfs(1, 0);
if(r1 && r2)
{
if(r1 > r2) swap(r1, r2);
cout << r1 << " " << r2;
}
else
{
cout << r1 << endl;
}
}
【树的直径】
树的直径 - 两次 dfs
【算法流程】
- 从任意结点 x 开始进行第一次 dfs,找到距离 x 最远的结点 y;
- 然后再从 y 结点开始再来一次 dfs,找到距离 y 最远的结点 z。
那么,y 到 z 就是直径的两端。
【性质】
- 在一棵树上,从任意节点 x 开始进行一次 dfs,找到距离 x 最远的结点必定为直径的一端。 如果存在负边权,上述性质将不再成立。因此,两次 dfs 只适用于所有边权全部非负的情况。
从任意一点开始找到最远的点,一定是直径的一端。
【设计 dfs】
- 我们发现,两次 dfs 执行的逻辑是一样的,因此只用设计一个 dfs 函数即可。
我们想要的是根节点与其余任意点的距离 d。从根节点递归的过程中,从 x 进入 y 结点之前,就可以更新出距离:dy = dx + w(x,y),然后再递归到 y 结点中即可。 用一些全局变量维护最大距离,以及对应的结点即可。
【顺便还可以维护出直径所在的路线】
- 每次递归传入父结点,额外用一个数组维护父结点,还能还原出直径所在的路线。
【模板题】
B4016 树的直径 - 洛谷
https://www.luogu.com.cn/problem/B4016
【代码实现(带边权,可还原路径)】
cpp
#include <bits/stdc++.h>
using namespace std;
const int N = 5e4 + 10;
int n;
vector<int> edges[N];
int r1, r2; // 最多有两个重心
int ret = N; // 记录所有最大子树的最小值
int f[N]; // 以x为根的子树的大小
void dfs(int x, int fa)
{
f[x] = 1;
int m = 0;
for(auto y : edges[x])
{
if(y == fa) continue;
dfs(y, x);
m = max(m, f[y]);
f[x] += f[y];
}
m = max(m, n - f[x]);
if(m < ret)
{
r1 = x;
r2 = 0;
ret = m;
}
else if(m == ret)
{
r2 = x;
}
}
signed main()
{
cin >> n;
for(int i = 1; i < n; i++)
{
int u, v; cin >> u >> v;
edges[u].push_back(v);
edges[v].push_back(u);
}
dfs(1, 0);
if(r1 && r2)
{
if(r1 > r2) swap(r1, r2);
cout << r1 << " " << r2;
}
else
{
cout << r1 << endl;
}
}
树的直径 - 树形 dp
【算法原理】
对于一棵无根树,把它看作是以 1 为根节点的有根树,求出经过 x 的最长路径:
- 如果这条路径既向上,又向下,是很难求的;
- 可以固定这条路径只在以 x 为根的子树中考虑。因为向上的路线,会在回溯到父节点的时候考虑进去;
- 那么,对于 x 结点的孩子结点 y,需要提供从 y 开始向下走的,能走到的最长路径;
因此,树形 dp 的逻辑为:
状态表示:fx 表示从 x 出发,只往下走的最长路径;
状态转移方程:fx = max(fx, fy+w(x,y))。
如何找出经过 x 的最长路径呢?遍历所有孩子,找出 fy1 + w(x,y1) + fy2 +w(x,y2)) 的最大值;
其实不用那么麻烦。在树形 dp 的过程中,当我们从 yi 回溯时,fx 尚未更新,里面存储着前面所有路径的最大值。那么,在用 yi 更新 fx 之前,就能求出两条路径之和的最大值。
【模板题】
U81904 【模板】树的直径 - 洛谷
https://www.luogu.com.cn/problem/U81904
【代码实现(树形 DP,带边权)】
cpp
int n;
vector<PII> edges[N];
int f[N], ret;
void dfs(int x, int fa)
{
for(auto& t : edges[x])
{
int y = t.first, z = t.second;
if(y == fa) continue;
dfs(y, x);
ret = max(ret, f[x] + f[y] + z);
f[x] = max(f[x], f[y] + z);
}
}
void fun()
{
dfs(1, 0);
cout << ret << endl;
}