哈喽大家好,我是 doooge。今天我们要将数论中的一个算法-树的直径。

\[\Huge 树的直径 详解
\]

1.树的直径是什么

这是一棵图论中的树:

这棵树的直径就是这棵树中最长的一条简单路径

2.树的直径怎么求

2.1暴力算法

直接对每个点进行 DFS,找到每个点离最远的点的距离,最后求出最长的一条路,也就是树的直径。

时间复杂度:\(O(n^2)\)

代码我就只放 DFS 的了,其他的没什么必要:

void dfs(int x,int fa,int sum){
dis[x]=sum;
for(auto i:v[x]){
if(i==fa)continue;
dfs(i,x,sum+1);
}
return;
}

重点:2.2 DFS直接求

直接说结论:

对于每一个点 \(x\),离 \(x\) 最远的点一定是树的直径的一个顶点。

为什么呢?

我们可以用反证法来推导:

假设树的直径的端点为 \(u\) 和 \(v\),设对于每一个离点 \(x\) 最远的点 \(y\) 不是树的直径的端点 \(u\) 和 \(v\),按我们可以分类讨论(以下把点 \(x\) 到点 \(y\) 的路径称作 \(x \to y\),它们的距离称作 \(dis_{x \to y}\)):

  1. 点 \(x\) 在树的直径 \(u \to v\) 中
  2. 点 \(x\) 不在树的直径 \(u \to v\) 中,但 \(x \to y\) 这条路径与树的直径 \(u \to v\) 有一部分重合。
  3. 点 \(x\) 不在树的直径 \(u \to v\) 中,且 \(x \to y\) 这条路径与树的直径 \(u \to v\) 完全不重合。

(温馨提示:下面的内容建议自己先推一遍,画棵树想想再看)

先来看情况 \(1\),若点 \(x\) 在树的直径 \(u \to v\) 中且点 \(y\) 既不等于 \(u\) 也不等于 \(v\)。

因为 \(y\) 既不等于 \(u\) 也不等于 \(v\),那么 \(dis_{x \to y}\) 必定会大于 \(dis_{x \to u}\) 和 \(dis_{x \to v}\),因为 \(dis_{u \to v} = dis_{u \to x} + dis_{x \to v}\),又因为 \(dis_{x \to v} < dis_{x \to y}\),那么此时这棵树的直径便是 \(u \to y\) 这两条路,与直径的定以不符,所以错误。

再来看情况 \(2\),点 \(x\) 不在树的直径 \(u \to v\) 中,但 \(x \to y\) 这条路径与树的直径 \(u \to v\) 有一部分重合。这里又可以分成两种情况。

  1. \(x \to y\) 被完全包含在 \(u \to v\) 内,这是显然不可能的。
  2. \(x \to y\) 有一部分包含在 \(u \to v\) 内,那我们可以设点 \(o\) 为公共部分其中的一个点,那么此时 \(dis_{o \to y}\) 一定要大于 \(dis_{o \to v}\) 和 \(dis_{o \to u}\),与直径的定以不符,所以错误。

最后来看情况 \(3\),点 \(x\) 不在树的直径 \(u \to v\) 中,且 \(x \to y\) 这条路径与树的直径 \(u \to v\) 完全不重合。

这时,我们设点 \(o\) 于 \(u \to v\) 内,因为每棵树都是连通的,所以必定有一条 \(x \to o\) 路。于是,就得到了一下式子:

\[dis_{u \to v}=dis_{u \to o}+dis_{o \to v}=dis_{u \to o}+dis_{x \to v}-dis_{x \to o}
\]
\[dis_{u \to y}=dis_{u \to o}+dis_{o \to y}=dis_{u \to o}+dis_{x \to y}-dis_{x \to o}
\]

将两个式子互相抵消,分别得到 \(dis_{x \to v}\) 和 \(dis_{x \to y}\),因为 \(dis_{x \to y} > dis_{x \to v}\),所以得到 \(dis_{u \to v} < dis_{u \to y}\),与直径的定以不符,所以错误。

至此,证毕。

于是!我们可以从点 \(1\) 开始 DFS,找到离点 \(1\) 最远的点 \(y\),再进行 DFS 找到离点 \(y\) 最远的点,就找到了树的直径。

代码:

#include<bits/stdc++.h>
using namespace std;
int dis,pos;
vector<int>v[100010];
void dfs(int x,int fa,int sum){
if(sum>=dis){//注意这里一定是>=而不是>
dis=sum;
pos=x;
}
for(auto i:v[x]){
if(i==fa)continue;//不能走回头路
dfs(i,x,sum+1);
}
return;
}
int main(){
int n;
cin>>n;
for(int i=1;i<n;i++){
int x,y;
cin>>x>>y;
v[x].push_back(y);
v[y].push_back(x);
}
dfs(1,-1,0);//找出点y
dis=0;//记得清空dis变量
dfs(pos,-1,0);
cout<<dis<<endl;
return 0;
}

该模版写法不一,也可以用 \(dis\) 数组存储距离,DFS 完后再找最大的路径。该带码也同样适用于带边权的树。

时间复杂度:\(O(n)\)。

注意:该算法只能在所有边权为正数的情况下成立,否则会出问题,具体为什么下面会讲

我们来看这张图:

不难发现,这棵树的直径是 \(5 \to 6\) 这一条路,但是如果你从点 \(1\) 开始进行 DFS,只能找到点 \(3\),因为中间被 \(2 \to 4\) 这条边挡住了,从 \(1 \to 5\) 不是最优解。

方法3:树形DP

主播主播,你的 DFS 大法确实很强,但是还是太吃条件了,有没有既速度快又没有限制的算法呢?

有的兄弟,有的,像这样的算法,主播还有一个,都是 T0.5 的强势算法,只要你掌握一个,就能秒杀 CSP 树的直径,如果想主播这样两个都会的话,随便进 NOI CSP-S。

好了,回归正题,我们来讲讲树形DP 的写法。\(dp_x\) 表示从 \(x\) 出发走向 \(x\) 的子树中最长的一条路径。

假设有一棵树的根节点为 \(root\)(我们这里称把 \(x\) 的子节点称作为 \(x_i\)),那么我们的 \(dp_{root}\) 就表示从 \(root\) 节点出发能走到的最远距离,也就是 \(root\) 的子树的最大的深度。所以,我们得要从子树开始更新,也就是在这里:

for(auto i:v[x]){//继续dfs
if(i.x==fa)continue;//不能走回头路
dfs(i.x);//往下搜索
dp[x]=...;//这里开始更新,此时先dfs的子节点会先更新dp
}

那么,我们就可以在遍历子节点 \(v_i\) 的时候更行新 \(dp_{root}\):

\[dp_{root}=\max(dp_{root},dp_{v_i}+dis_{root \to v_i})
\]

其他节点也同理。

这时,有聪明的读者就会说了:你这不是只更新了它的一个子树吗,如果树的直径是这样子,那你的 DP 不是就错了吗?

读者说的没错,我们要考虑图片上的情况。

我们可以设置一个中间节点,比如这张图的中间点就是 \(root\) 节点,一条路径可以贯穿一个中间节点的两个子树,而我们的 \(dp\) 数组只记录了一个子树的最大的深度,也就是子树的最长路。

于是,我们可以在更新 \(dp\) 数组的时候同时更新另一个变量 \(ans_x\),表示若 \(x\) 为树的直径的中间点,穿过 \(x\) 最长的路径的长度。当然,\(dp\) 数组也不能落下,但是答案还是存在 \(ans\) 数组里。因为要找到两个长度最大的长度,所以更新代码为这样:

\[ans_x=\max(ans_x,dp_x+dp_{v_i}+dis_{x
\to v_i})\]

至于为什么是 \(dp_x+dp_{v_i}\) 因为此时的 \(dp_x\) 表示的是在 \(v_i\) 之前遍历到的子树的最大值,\(dp_{v_i}+dis_{x \to v_i}\) 表示这棵子树的最大的长度,所以,\(ans\) 数组的更新应该在 \(dp\) 数组的更新之前。

代码(我只展示 DFS 部分,剩下的应该不难了吧):

void dfs(int x,int fa){
dp[x]=0;
for(auto i:v[x]){//'v'是一个结构体vector,里面包含x和w这两个参数
if(i.x==fa)continue;//i.x表示遍历到的节点
dfs(i.x,x);//继续搜索下去
ans[x]=max(ans[x],dp[x]+dp[i.x]+i.w)//i.w表示x到i.x的边权
dp[x]=max(dp[x],dp[i.x]+i.w);
}
}

3.例题

T1.P8602 [蓝桥杯 2013 省 A] 大臣的旅费

题目描述

很久以前,T 王国空前繁荣。为了更好地管理国家,王国修建了大量的快速路,用于连接首都和王国内的各大城市。

为节省经费,T 国的大臣们经过思考,制定了一套优秀的修建方案,使得任何一个大城市都能从首都直接或者通过其他大城市间接到达。同时,如果不重复经过大城市,从首都到达每个大城市的方案都是唯一的。

J 是 T 国重要大臣,他巡查于各大城市之间,体察民情。所以,从一个城市马不停蹄地到另一个城市成了 J 最常做的事情。他有一个钱袋,用于存放往来城市间的路费。

聪明的 J 发现,如果不在某个城市停下来修整,在连续行进过程中,他所花的路费与他已走过的距离有关,在走第 \(x - 1\) 千米到第 \(x\) 千米这一千米中(\(x\) 是整数),他花费的路费是 \(x+10\) 这么多。也就是说走 \(1\) 千米花费 \(11\),走 \(2\) 千米要花费 \(23\)。

J 大臣想知道:他从某一个城市出发,中间不休息,到达另一个城市,所有可能花费的路费中最多是多少呢?

(绝对不是水字数)

思路+代码

这道题乍一看上去确实很乱,但我们可以找找关键句(跟语文课上学的一样)。

如果不重复经过大城市,从首都到达每个大城市的方案都是唯一的。 咦?这句话的意思不就是从根节点出发到每一个节点的路径唯一吗?

他从某一个城市出发,中间不休息,到达另一个城市,所有可能花费的路费中最多是多少呢 咦?这句话不就是要求一棵树上最长的一条路径吗?

综上所述,这道题完完全全就是树的直径的板子,只是读题困难一点而已。需要注意,最后的答案并不是树的直径的长度,而是像题目描述中的这样:

cout<<dis*10+(dis+1)*dis/2<<endl;

OK,这道题就没有其他的坑了,代码如下:

#include<bits/stdc++.h>
#define int long long
using namespace std;
int dis,pos;
struct ll{//个人习惯,见谅
int x,w;
};
vector<ll>v[100010];
void dfs(int x,int fa,int sum){
if(sum>=dis){
dis=sum;
pos=x;
}
for(auto i:v[x]){
if(i.x==fa)continue;
dfs(i.x,x,sum+i.w);
}
return;
}
signed main(){
int n;
cin>>n;
for(int i=1;i<n;i++){
int x,y,w;
cin>>x>>y>>w;
v[x].push_back({y,w});
v[y].push_back({x,w});
}
dfs(1,-1,0);
dis=0;
dfs(pos,-1,0);
cout<<dis*10+(dis+1)*dis/2<<endl;
return 0;
}

难度:\(1/5\)。

T2.HDU 2196 Computer

请注意,这道题不是洛谷的,需要在 vjudge 上交代码。

题目描述

给定一棵节点为 \(N\) 的树(\(1 \le N \le 10^4\)),输出每个节点 \(i\) 离 \(i\) 最远的节点的长度。

思路+代码

首先,\(O(N^2)\) 的暴力 DFS 是不可能的,因为题目中还有 \(T\) 组数据。想一想,对于每个节点 \(i\) 离 \(i\) 最远的点是什么呢?

对的,之前说过,就是树的直径的两个端点!所以离每一个节点 \(i\) 最远的点就是树的直径的两端的节点 \(u\) 和 \(v\)。

于是,我们可以用 \(O(N)\) 的 DFS 先将树的直径的两个端点求出来,在继续用 \(O(N)\) 的 DFS 求出对每个节点的距离,对于节点 \(i\),它的答案就是:

\[\max(dis_{u \to i},dis_{v \to i})
\]

代码:

#include<bits/stdc++.h>
using namespace std;
int dis[100010],n;
bool f[100010];
struct ll{
int x,w;
};
vector<ll>v[100010];
void dfs(int x,int sum){
dis[x]=max(dis[x],sum);
f[x]=true;
for(int i=0;i<v[x].size();i++){
ll tmp=v[x][i];
if(f[tmp.x])continue;
dfs(tmp.x,sum+tmp.w);
}
return;
}
void solve(){
memset(dis,0,sizeof(dis));
memset(f,false,sizeof(f));
for(int i=1;i<=n;i++){
v[i].clear();
}
for(int i=1;i<n;i++){
int x,w;
cin>>x>>w;
v[i+1].push_back({x,w});
v[x].push_back({i+1,w});
}
dfs(1,0);
int mx=-1e9,pos=-1,pos2=-1;
for(int i=1;i<=n;i++){
pos=(dis[i]>=mx?i:pos);
mx=max(mx,dis[i]);
}
memset(dis,0,sizeof(dis));
memset(f,false,sizeof(f));
dfs(pos,0);
mx=-1e9;
for(int i=1;i<=n;i++){
pos2=(dis[i]>=mx?i:pos2);
mx=max(mx,dis[i]);
}
memset(f,false,sizeof(f));
dfs(pos2,0);
for(int i=1;i<=n;i++){
cout<<dis[i]<<'\n';
}
}
int main(){
while(cin>>n){
solve();
}
return 0;
}

难度:\(3/5\)。

4.作业

  1. B4016 树的直径,模板题,难度:\(1/1\)。
  2. P3304 [SDOI2013] 直径,难度:\(3/5\)。
  3. P4408 [NOI2003] 逃学的小孩,难度\(4/5\)

5.闲话

蒟蒻不才,膜拜大佬,如果文章有什么问题,请在评论区@我。

[c++算法] 树的直径,包教包会!的更多相关文章

  1. 算法笔记--树的直径 && 树形dp && 虚树 && 树分治 && 树上差分 && 树链剖分

    树的直径: 利用了树的直径的一个性质:距某个点最远的叶子节点一定是树的某一条直径的端点. 先从任意一顶点a出发,bfs找到离它最远的一个叶子顶点b,然后再从b出发bfs找到离b最远的顶点c,那么b和c ...

  2. POJ 2631 Roads in the North(树的直径)

    POJ 2631 Roads in the North(树的直径) http://poj.org/problem? id=2631 题意: 有一个树结构, 给你树的全部边(u,v,cost), 表示u ...

  3. 51 nod 1427 文明 (并查集 + 树的直径)

    1427 文明 题目来源: CodeForces 基准时间限制:1.5 秒 空间限制:131072 KB 分值: 160 难度:6级算法题   安德鲁在玩一个叫“文明”的游戏.大妈正在帮助他. 这个游 ...

  4. 与图论的邂逅01:树的直径&基环树&单调队列

    树的直径 定义:树中最远的两个节点之间的距离被称为树的直径.  怎么求呢?有两种官方的算法(不要问官方指谁我也不晓得): 1.两次搜索.首先任选一个点,从它开始搜索,找到离它最远的节点x.然后从x开始 ...

  5. 树的最长链-POJ 1985 树的直径(最长链)+牛客小白月赛6-桃花

    求树直径的方法在此转载一下大佬们的分析: 可以随便选择一个点开始进行bfs或者dfs,从而找到离该点最远的那个点(可以证明,离树上任意一点最远的点一定是树的某条直径的两端点之一:树的直径:树上的最长简 ...

  6. Codeforces 592D - Super M - [树的直径][DFS]

    Time limit 2000 ms Memory limit 262144 kB Source Codeforces Round #328 (Div. 2) Ari the monster is n ...

  7. F - Warm up HDU - 4612 tarjan缩点 + 树的直径 + 对tajan的再次理解

    题目链接:https://vjudge.net/contest/67418#problem/F 题目大意:给你一个图,让你加一条边,使得原图中的桥尽可能的小.(谢谢梁学长的帮忙) 我对重边,tarja ...

  8. 【TYVJ】1520 树的直径

    [算法]树的直径 memset(a,0,sizeof(a)) #include<cstdio> #include<algorithm> #include<cstring& ...

  9. 浅谈关于树形dp求树的直径问题

    在一个有n个节点,n-1条无向边的无向图中,求图中最远两个节点的距离,那么将这个图看做一棵无根树,要求的即是树的直径. 求树的直径主要有两种方法:树形dp和两次bfs/dfs,因为我太菜了不会写后者这 ...

  10. 树的直径-CF592D Super M

    给定一颗n个节点树,边权为1,树上有m个点被标记,问从树上一个点出发,经过所有被标记的点的最短路程(起终点自选).同时输出可能开始的编号最小的那个点.M<=N<=123456. 先想:如果 ...

随机推荐

  1. 终于有人把ROS机器人操作系统讲明白了

    终于有人把ROS机器人操作系统讲明白了 导读:机器人是多专业知识交叉的学科,通常涉及传感器.驱动程序.多机通信.机械结构.算法等,为了更高效地进行机器人的研究和开发,选择一个通用的开发框架非常必要,R ...

  2. 使用Python计算并可视化长直导线产生的磁场

    引言 大家好,今天我们来探讨一个有趣的话题--长直导线产生的磁场,并通过 Python 来进行计算和可视化.你可能会问,为什么要研究这个问题?其实,这是电磁学中的一个基础问题,理解了它,我们就能更好地 ...

  3. 深入理解Java虚拟机-线程安全与锁优化

    线程安全级别 级别 描述 示例 不可变(Immutable) 对象状态不可变,天然线程安全. String.Integer 绝对线程安全 所有操作都线程安全(Java 中极少见). Vector(通过 ...

  4. MySQL插入异常:SQL state [HY000]; error code [1366]-----(utf8mb4)

    发现爬虫软件,爬取数据不及时,查询服务器日志发现异常: SQL state [HY000]; error code [1366] java.sql.SQLException: Incorrect st ...

  5. MySQL 中的事务隔离级别有哪些?

    MySQL 中的事务隔离级别有哪些? 在 MySQL 中,事务隔离级别用于定义一个事务能看到其他事务未提交的数据的程度.MySQL 支持以下四种事务隔离级别,每种级别对并发操作的支持程度和一致性要求不 ...

  6. springboot分页查询并行优化实践

    --基于异步优化与 MyBatis-Plus 分页插件思想的实践 适用场景 数据量较大的单表分页查询 较复杂的多表关联查询,包含group by等无法进行count优化较耗时的分页查询 技术栈 核心框 ...

  7. DeepSeek+Coze实战:如何从0到1打造一个热点监控智能体

    大家好,我是汤师爷,专注AI智能体分享~ 短视频小白经常会遇到这样的困扰. 每天花大量时间刷视频,想要找到你所在赛道的爆款内容,却总是难以系统地整理和分析? 想要批量获取某个关键词的爆款视频数据,但是 ...

  8. Web前端入门第 50 问:CSS 内容溢出怎么处理?

    溢出:盒模型装不下内容的时候,超出盒子大小的内容就称之为内容溢出,这里的内容又分为盒模型和文本,所以 CSS 在处理溢出时候也分为文本和盒模型两种情况. 正常情况内容溢出应该换行自动撑开盒子大小,但某 ...

  9. 修改Tomcat默认端口方法

    找到Tomcat的配置文件conf目录下的server 例如我的具体地址:C:\moliy\code\resourse\apache-tomcat-9.0.13-windows-x64\apache- ...

  10. [RCTF2015]EasySQL 报错注入与二次注入

    [RCTF2015]EasySQL 报错注入与二次注入 二次注入,可以概括为以下两步: 第一步:插入恶意数据 进行数据库插入数据时,对其中的特殊字符进行了转义处理,在写入数据库的时候又保留了原来的数据 ...