树的直径
树的直径定义
给定一棵带权(边)树
- 距离: 两个点路径上的边权和
- 树的直径: 最大距离
两次dfs法求直径
- 从任意一点
出发,找到距离 最远的点 - 从点
出发,找到距离 最远的点 ,则 到 的路径就是树的直径
定理 1
从任意一点
证明定理 1
我们使用反证法来证明。
假设:定理不成立。即,从任意点
那么,我们可以进行如下推理:
- 设树上的一条直径为路径
,其长度为 。根据我们的假设,有 且 。 - 根据
的定义(距离 最远的点),对于树上任意一点 ,都有 。特别地,我们有 。
下点分情况讨论:
情况1:
显然可以得到:
- 于是得到
- 于是得到了一条比直径
更长的路径, 矛盾.
情况2:
- 因为树上的任意两个点都存在唯一的路径,
可以到达直径 上的任意一点, 设 到 的距离为 ,那么 点就是最近的点 - 显然可以得到
(因为 是 的最远点) - 于是得到
- 但是根据
的定义, - 矛盾
情况3:
同样的思路:
- 矛盾
因此,假设不成立,原定理得证。
证明
算法:
- 第一次 DFS 从任意点
找到最远点 ,根据引理, 是直径端点 - 第二次 DFS 从
找到最远点 ,则 就是直径
代码实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int max_dist, farthest_node;
void dfs_find_farthest(int u, int fa, int dist) {
if (dist > max_dist) {
max_dist = dist;
farthest_node = u;
}
for (int i = e.h[u]; i != -1; i = e[i].next) {
int v = e[i].v;
if (v == fa) continue;
dfs_find_farthest(v, u, dist + e[i].w);
}
}
int tree_diameter() {
// 第一次DFS:从任意点(如1)找到最远点
max_dist = 0;
dfs_find_farthest(1, 0, 0);
int v = farthest_node;
// 第二次DFS:从v找到最远点,距离即为直径
max_dist = 0;
dfs_find_farthest(v, 0, 0);
return max_dist;
}
时间复杂度:
DP求树的直径
我们认为(假定)树的根为
表示以点 为根的子树. 表示路径上的两个端点的lca为点 的路径的集合
根据公式
那么答案就是
于是问题转换成求
表示经过点 的最长链,且 是链的 (这句话约束了链的两个端点是叶子) 表示点 到叶结点的 最长链 表示点 的孩子 表示边 的长度
- 设
那么这个问题就变成的集合最大二值和问题
于是我们得到代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int ans;
template<typename T>
void upd(T& v,T t) { if( v < t) v = t; }
void dfs_dp(int u,int fa) {
int pathlca = 0;
for(int i = e.h[u];i != -1; i = e[i].next)
{
int v = e[i].v;
if( v == fa) continue;
dfs_dp(v, u);
int dt = d[v] + e[i].w; //得到 u到达 v 子树上的最远点
upd(pathlca,d[u] + dt); // 得到 分类 dt 的结尾的最值,更新 pathlca
upd(d[u],dt); // 更新d[u]
}
upd(ans,pathlca);
}
树的所有直径拥有相同的中点
反证法:
假设有两条不相交的直径
因为树上任意两点都有一条唯一的路径
那么假设连接两个中点的长度为len, 直径长度为max_d
那么两条直径的两端距离为max_d + len > max_d
那么直径长度不是len
矛盾
所以两条直径必须经过一个点
定理: 树的所有直径都经过相同的中点(或中点所在的边)。
证明
定义:
- 路径的中点:将路径分成两个相等长度的点(或边)
- 如果直径长度为偶数,中点是一个确定的点
- 如果直径长度为奇数,中点是一条确定的边
反证法证明:
假设存在两条直径
设:
为直径 的中点 为直径 的中点 - 直径长度为
情况1:中点都是点(直径长度为偶数)
由于
考虑以下四个距离:
(中点定义) (中点定义)
由于
则:
但
这意味着
这与
情况2:中点都是边(直径长度为奇数)
设
类似的论证可以证明,如果中点边不同,也能构造出比直径更长的路径,产生矛盾。
结论: 因此,树的所有直径必须有相同的中点。
推论与应用
- 树的中心: 所有直径的中点(或中点边)的集合称为树的中心
- 中心性质:
- 如果直径长度为偶数,中心是单个点
- 如果直径长度为奇数,中心是两个相邻的点(一条边)
- 应用: 这个性质在很多树算法中有重要应用,如:
- 找树的最小高度根节点
- 网络布局优化
- 树的重心相关问题
代码验证
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 验证所有直径是否经过相同中点
vector<int> diameter_path; // 存储直径路径
vector<int> center_points; // 存储中心点
void find_diameter_path(int u, int fa, int target, vector<int>& path) {
path.push_back(u);
if (u == target) return;
for (int i = e.h[u]; i != -1; i = e[i].next) {
int v = e[i].v;
if (v == fa) continue;
find_diameter_path(v, u, target, path);
if (path.back() == target) return;
}
path.pop_back();
}
void find_center() {
int v = find_farthest(1, 0);
int w = find_farthest(v, 0);
vector<int> path;
find_diameter_path(v, 0, w, path);
int diameter_length = path.size() - 1;
if (diameter_length % 2 == 0) {
// 中心是单个点
center_points.push_back(path[diameter_length / 2]);
} else {
// 中心是两个点
center_points.push_back(path[diameter_length / 2]);
center_points.push_back(path[diameter_length / 2 + 1]);
}
}
树的直径的重合部分
重要性质: 树的所有直径有且只有一段重合的部分,这段重合可能是:
- 一个点(当直径长度为偶数时)
- 一条边(当直径长度为奇数时)
证明
基于前面证明的"所有直径拥有相同的中点"性质:
-
所有直径都经过相同的中点区域
- 如果直径长度为偶数,所有直径都经过同一个中心点
- 如果直径长度为奇数,所有直径都经过同一条中心边
-
重合部分的唯一性
- 设树的中心区域为
(点或边) - 任意直径都必须完整包含
- 在
之外,不同直径可能分叉到不同的端点
- 设树的中心区域为
-
重合部分的最大性
- 重合部分不能更长,否则会有直径不包含某个端点
- 重合部分不能更短,否则会有直径不经过中心
直观理解
想象一棵树的形状:
A B C
\ / \ /
\ / \ /
D E
/ \ / \
F G H I
/ \ / \ / \
J K L M N O
- 如果直径长度为偶数,所有直径都经过中心点 D
- 如果直径长度为奇数,所有直径都经过中心边 (D,E)
应用:NOI2013 快餐店
这是一个经典的应用题,需要在树上设置一个快餐店,使得所有点到快餐店的最大距离最小。
解题思路:
- 找到所有直径的公共重合部分
- 将快餐店设在重合部分的中点
- 这样可以最大化地减少最远距离
代码实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 找到所有直径的重合部分
vector<int> find_common_path() {
// 找到一条直径的端点
int v = find_farthest(1, 0);
int w = find_farthest(v, 0);
// 找到所有直径端点
vector<int> diameter_ends = find_all_diameter_ends(v, w);
// 找到重合部分的起点和终点
int common_start = n, common_end = 1;
for (int end : diameter_ends) {
vector<int> path1, path2;
find_path(v, end, path1);
find_path(w, end, path2);
// 找到两条路径的公共部分
int lca_node = lca(v, end);
// 更新重合部分的边界
update_common_boundary(path1, path2, common_start, common_end);
}
return get_common_path(common_start, common_end);
}
// 快店店问题的核心代码
double solve_restaurant() {
vector<int> common_path = find_common_path();
if (common_path.size() == 1) {
// 重合部分是一个点
return max_distance_from_point(common_path[0]);
} else {
// 重合部分是一条路径
return max_distance_from_path(common_path);
}
}
相关题目
- [NOI2013 快餐店]: 在树上找一点,使得到所有点的最大距离最小
- [CF 1059E] Split the Tree: 利用直径重合性质进行树的分割
- [POJ 1849] Two: 两个人从树的任意两点出发,走完所有边
总结
这个性质告诉我们:
- 树的直径虽然可能有很多条,但它们有很强的规律性
- 所有直径都会在树的"中心区域"重合
- 这个性质是解决很多树问题的基础,特别是涉及最优化的问题
- 在实际应用中,可以利用这个性质来简化问题的复杂度
直径必过边
对应题目: luogu P3304 SDOI2013 直径
两次 dfs 法
DP 法
参考
NOI2013有个叫快餐店
最终模板代码:
/code/tree/diameter_with_dijkstra.cpp.ENOENT: no such file or directory, open '/home/runner/work/rbook_nunjucks/rbook_nunjucks/packages/code/tree/diameter_with_dijkstra.cpp'