【学习笔记】树的直径

树的直径

概述

树的直径指树上距离最远的两点间的距离,它在树上问题上有许多应用,往往通过树的直径的性质可以将一个高时间复杂度的解法变为线性求解。

定义:树中所有最短路径最大值

求法

贪心法:

两次bfs/dfs:先从任意一点P出发,找离它最远的点Q,再从点Q出发,找离它最远的点W,W到Q的距离就是是的直径,其中找离某个点最远的点,可利用bfs/dfs简单求出。

Code:
void dfs1(int rt, int father, int road){
dis[rt] = road;
if(dis[rt] > max_dis){
max_dis = dis[rt];
ed = rt;
}
for(register int i = head[rt]; i; i = e[i].next){
int v = e[i].to, w = e[i].dis;
if(v == father) continue;
dfs1(v, rt, road + w);
}
} //随意找一点, 找到离它最远的一点
void dfs2(int rt, int father, int road){
dis[rt] = road;
fa[rt] = father;
for(register int i = head[rt]; i; i = e[i].next){
int v = e[i].to, w = e[i].dis;
if(v == father) continue;
dfs2(v, rt, depth + w);
}
} //从离它最远的一点往回推,即可找到直径
树形dp:

f1[x] 表示在以x为根的子树中,x到叶子结点距离的最大值,
f2[x] 表示在以x为根的子树中,x到叶子结点距离的次大值,
注意这里的最长与次长路径是没有重合部分的,考虑所有最浅点为x的路径,其对答案的更新显然为 f1[x]+f2[x]
f1[x]f2[x] 的转移方程也易得,故可求出其直径。

性质

  1. 直径两端点一定是两个叶子节点。

  2. 某个距离任意点最远的点一定是某个直径的一个端点。

  3. 对于两棵树,如果第一棵树直径两端点为 u , v,第二棵树直径两端点为 x , y ,用一条边将两棵树连接,那么新树的直径一定是 u , v , x , y ,中的两个点。
    证明:如果新树直径不是原来两棵树中一棵的 直径,那么新直径一定经过两棵树的连接边,新直径在原来每棵树中的部分一定是距离连接点最远的点,即一定是原树直径的一个端点

  4. 对于一棵树,如果在一个点的上接一个叶子节点,那么最多会改变直径的一个端点。
    证明:假设在 x 下面接一个点 y ,直径变成了 (u,x) ,原树直径为 (a,b) ,那么 dis(u,x)>dis(a,b) , dis(u,x)=dis(u,y)+1dis(u,y)+1>dis(a,b) ,如果 dis(u,y)<dis(a,b) ,那么显然不成立;如果 dis(u,y)=dis(a,b) ,那么 (u,y) 也是原树的直径,符合上述结论。

  5. 若一棵树存在多条直径,那么这些直径交于一点且交点是这些直径的中点。

例题

P5536 【XR-3】核心城市

树的直径板子。
当取一个点 a ,最远端点是直径的端点,最大距离 da 与两直径端点距离的较大值,那么当 a 作为直径的中点时, d最小,那 a 就可以确定为一个核心城市。
选剩下的城市的时候,要尽量和已经选了的核心城市相连,用贪心的思想,尽量使得选完后的 d 变小。

总结一下,要选的核心城市就是按照当前点能走到的最远距离 dis 降序排序之后,在一个一个选。其中设 d[i] 为当前深度, maxd[i]i 能走到的最深的深度。那么 dis[i]=maxd[i]d[i]

按照 dis 排序之后,前 k 个点是核心城市,找到非核心城市中 dis 最大的那个值, dis[k+1]+1 就是答案。

Code
点击查看代码
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int MAXN = 1e5 + 10;
int n, k, cnt, max_r, pos, mid, ans;
int head[MAXN], deep[MAXN], fa[MAXN];
int max_deep[MAXN], dis[MAXN];
struct Edge{
int to, next;
}e[MAXN << 1];
inline void Add(int u, int v){
e[++cnt].to = v;
e[cnt].next = head[u];
head[u] = cnt;
}
void dfs1(int rt, int father, int depth){
deep[rt] = depth;
if(deep[rt] > max_r){
max_r = deep[rt];
pos = rt;
}
for(register int i = head[rt]; i; i = e[i].next){
int v = e[i].to;
if(v == father) continue;
dfs1(v, rt, depth + 1);
}
}
void dfs2(int rt, int father, int depth){
fa[rt] = father;
deep[rt] = depth;
if(deep[rt] > max_r){
max_r = deep[rt];
pos = rt;
}
for(register int i = head[rt]; i; i = e[i].next){
int v = e[i].to;
if(v == father) continue;
dfs2(v, rt, depth + 1);
}
}
void dfs3(int rt, int father, int depth){
deep[rt] = depth;
max_deep[rt] = deep[rt];
for(register int i = head[rt]; i; i = e[i].next){
int v = e[i].to;
if(v == father) continue;
dfs3(v, rt, depth + 1);
max_deep[rt] = max(max_deep[rt], max_deep[v]);
}
}
inline bool cmp(const int &a, const int &b){
return a > b;
}
inline int read(){
int x = 0, f = 1;
char c = getchar();
while(c < '0' || c > '9'){
if(c == '-') f = -1;
c = getchar();
}
while(c >= '0' && c <= '9'){
x = (x << 1) + (x << 3) + (c ^ 48);
c = getchar();
}
return x * f;
}
int main(){
n = read(), k = read();
for(register int i = 1; i <= n - 1; i++){
int u, v;
u = read(), v = read();
Add(u, v);
Add(v, u);
}
dfs1(1, 0, 1);
max_r = 0;
memset(deep, 0, sizeof(deep));
dfs2(pos, 0, 1);
mid = pos;
for(register int i = 1; i <= deep[pos] / 2; i++)
mid = fa[mid];
memset(deep, 0, sizeof(deep));
dfs3(mid, 0, 1);
for(register int i = 1; i <= n; i++)
dis[i] = max_deep[i] - deep[i];
sort(dis + 1, dis + 1 + n, cmp);
printf("%d", dis[k + 1] + 1);
return 0;
}
P4408 [NOI2003] 逃学的小孩

构成一颗树,就满足如果找一个点出去两条路径使它们的合最大,那么一条是直径时一定会存在一种最大的方案

题意虽然说先去谁家再去谁家,但是我们不需要管这个,因为 ABC 三个点我们可以任意互相交换它们所代表的对象,所以题目要求的就是在一棵树上找到3个点 ABCdis(A,B)+dis(B,C) 最大,同时要满足 dis(A,C)>dis(A,B)

所以我们可以使要找的两条路径其中一条是直径(设为 dis(A,B) ),然后枚举剩下的点,找到一个到达直径端点最长的另一条路径,不过因为题目要满足一个先去近的点再去远的点,所以我们需要在每次枚举的时候(设为 C ),选择 dis(A,B)dis(B,C) 的较小的一条边作为另一条路径。

Code
点击查看代码
#include<cstring>
#include<algorithm>
using namespace std;
const int MAXN = 2e5 + 10;
int n, m, cnt, st, ed;
long long max_st, max_ed, max_c, ans;
int head[MAXN];
long long dis_st[MAXN], dis_ed[MAXN];
struct Edge{
int to, next;
long long dis;
}e[MAXN << 1];
inline void Add(int u, int v, long long w){
e[++cnt].to = v;
e[cnt].dis = w;
e[cnt].next = head[u];
head[u] = cnt;
}
void dfs_pos_st(int rt, int father, long long road){
dis_st[rt] = road;
if(dis_st[rt] > max_st){
max_st = dis_st[rt];
st = rt;
}
for(register int i = head[rt]; i; i = e[i].next){
int v = e[i].to;
long long w = e[i].dis;
if(v == father) continue;
dfs_pos_st(v, rt, road + w);
}
}
void dfs_pos_ed(int rt, int father, long long road){
dis_ed[rt] = road;
if(dis_ed[rt] > max_ed){
max_ed = dis_ed[rt];
ed = rt;
}
for(register int i = head[rt]; i; i = e[i].next){
int v = e[i].to;
long long w = e[i].dis;
if(v == father) continue;
dfs_pos_ed(v, rt, road + w);
}
}
void dfs_dis_st(int rt, int father, long long road){
dis_st[rt] = road;
for(register int i = head[rt]; i; i = e[i].next){
int v = e[i].to;
long long w = e[i].dis;
if(v == father) continue;
dfs_dis_st(v, rt, road + w);
}
}
void dfs_dis_ed(int rt, int father, long long road){
dis_ed[rt] = road;
for(register int i = head[rt]; i; i = e[i].next){
int v = e[i].to;
long long w = e[i].dis;
if(v == father) continue;
dfs_dis_ed(v, rt, road + w);
}
}
inline long long read(){
long long x = 0, f = 1;
char c = getchar();
while(c < '0' || c > '9'){
if(c == '-') f = -1;
c = getchar();
}
while(c >= '0' && c <= '9'){
x = (x << 1) + (x << 3) + (c ^ 48);
c = getchar();
}
return x * f;
}
int main(){
n = read(), m = read();
for(register int i = 1; i <= m; i++){
int u, v, w;
u = read(), v = read(), w = read();
Add(u, v, w);
Add(v, u, w);
}
dfs_pos_st(1, 0, 0);
dfs_pos_ed(st, 0, 0);
ans += dis_ed[ed];
dfs_dis_st(st, 0, 0);
dfs_dis_ed(ed, 0, 0);
for(register int i = 1; i <= n; i++){
long long road = min(dis_st[i], dis_ed[i]);
if(max_c < road) max_c = road;
}
ans += max_c;
printf("%lld", ans);
return 0;
}
posted @   TSTYFST  阅读(111)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示