[c++算法] 树的直径,包教包会!
哈喽大家好,我是 doooge。今天我们要将数论中的一个算法-树的直径。
\]
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}\)):
- 点 \(x\) 在树的直径 \(u \to v\) 中
- 点 \(x\) 不在树的直径 \(u \to v\) 中,但 \(x \to y\) 这条路径与树的直径 \(u \to v\) 有一部分重合。
- 点 \(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\) 有一部分重合。这里又可以分成两种情况。
- \(x \to y\) 被完全包含在 \(u \to v\) 内,这是显然不可能的。
- \(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_{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\) 节点,一条路径可以贯穿一个中间节点的两个子树,而我们的 \(dp\) 数组只记录了一个子树的最大的深度,也就是子树的最长路。
于是,我们可以在更新 \(dp\) 数组的时候同时更新另一个变量 \(ans_x\),表示若 \(x\) 为树的直径的中间点,穿过 \(x\) 最长的路径的长度。当然,\(dp\) 数组也不能落下,但是答案还是存在 \(ans\) 数组里。因为要找到两个长度最大的长度,所以更新代码为这样:
\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\),它的答案就是:
\]
代码:
#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.作业
- B4016 树的直径,模板题,难度:\(1/1\)。
- P3304 [SDOI2013] 直径,难度:\(3/5\)。
- P4408 [NOI2003] 逃学的小孩,难度\(4/5\)
5.闲话
蒟蒻不才,膜拜大佬,如果文章有什么问题,请在评论区@我。
[c++算法] 树的直径,包教包会!的更多相关文章
- 算法笔记--树的直径 && 树形dp && 虚树 && 树分治 && 树上差分 && 树链剖分
树的直径: 利用了树的直径的一个性质:距某个点最远的叶子节点一定是树的某一条直径的端点. 先从任意一顶点a出发,bfs找到离它最远的一个叶子顶点b,然后再从b出发bfs找到离b最远的顶点c,那么b和c ...
- POJ 2631 Roads in the North(树的直径)
POJ 2631 Roads in the North(树的直径) http://poj.org/problem? id=2631 题意: 有一个树结构, 给你树的全部边(u,v,cost), 表示u ...
- 51 nod 1427 文明 (并查集 + 树的直径)
1427 文明 题目来源: CodeForces 基准时间限制:1.5 秒 空间限制:131072 KB 分值: 160 难度:6级算法题 安德鲁在玩一个叫“文明”的游戏.大妈正在帮助他. 这个游 ...
- 与图论的邂逅01:树的直径&基环树&单调队列
树的直径 定义:树中最远的两个节点之间的距离被称为树的直径. 怎么求呢?有两种官方的算法(不要问官方指谁我也不晓得): 1.两次搜索.首先任选一个点,从它开始搜索,找到离它最远的节点x.然后从x开始 ...
- 树的最长链-POJ 1985 树的直径(最长链)+牛客小白月赛6-桃花
求树直径的方法在此转载一下大佬们的分析: 可以随便选择一个点开始进行bfs或者dfs,从而找到离该点最远的那个点(可以证明,离树上任意一点最远的点一定是树的某条直径的两端点之一:树的直径:树上的最长简 ...
- Codeforces 592D - Super M - [树的直径][DFS]
Time limit 2000 ms Memory limit 262144 kB Source Codeforces Round #328 (Div. 2) Ari the monster is n ...
- F - Warm up HDU - 4612 tarjan缩点 + 树的直径 + 对tajan的再次理解
题目链接:https://vjudge.net/contest/67418#problem/F 题目大意:给你一个图,让你加一条边,使得原图中的桥尽可能的小.(谢谢梁学长的帮忙) 我对重边,tarja ...
- 【TYVJ】1520 树的直径
[算法]树的直径 memset(a,0,sizeof(a)) #include<cstdio> #include<algorithm> #include<cstring& ...
- 浅谈关于树形dp求树的直径问题
在一个有n个节点,n-1条无向边的无向图中,求图中最远两个节点的距离,那么将这个图看做一棵无根树,要求的即是树的直径. 求树的直径主要有两种方法:树形dp和两次bfs/dfs,因为我太菜了不会写后者这 ...
- 树的直径-CF592D Super M
给定一颗n个节点树,边权为1,树上有m个点被标记,问从树上一个点出发,经过所有被标记的点的最短路程(起终点自选).同时输出可能开始的编号最小的那个点.M<=N<=123456. 先想:如果 ...
随机推荐
- 【SpringCloud】SpringCloud config分布式配置中心
SpringCloud config分布式配置中心 概述 分布式系统面临的---配置问题 微服务意味着要将单体应用中的业务拆分成一个个子服务 ,每个服务的粒度相对较小,因此系统中会出现大量的服务.由于 ...
- 【SpringCloud】OpenFeign服务接口调用
OpenFeign服务接口调用 概述 我的理解: feign 为什么叫伪装? Feign可以把Rest的请求进行隐藏,伪装成类似SpringMVC的Controller一样.你不用再自己拼接url,拼 ...
- pnpm create vite my-react-app --template react-ts
npm install -D pnpm npm install -D vite pnpm create vite my-react-app --template react-ts npx vite - ...
- 二叉树专题学习(C++版) 基础的上机题
前言: 由于二叉树这一章的题型比较多,涉及到的递归程序也较多,所以单开一个随笔来记录这个学习过程,希望对读者有帮助. 理论知识基础 在二叉树的选择题中,常常会涉及到对于最多或最少结点.最大或最小高度. ...
- AndrodStudio构建时报错Could not find com.android.tools.build:gradle:xxx
检查配置的版本号在maven仓库里有没有. maven仓库地址: https://repo1.maven.org/maven2/com/android/tools/build/gradle/ 选择需要 ...
- Python读取CSV文件并存储到MySQL
在项目中对后台进行测试时,经常会遇到要在数据库新增数据,那么如何快速新增数据来提高工作效率呢? 现整理如下: 代码内容(csv_to_mysql.py): # coding=utf-8import p ...
- 『Plotly实战指南』--布局进阶篇
在数据可视化领域,Plotly的子图布局是打造专业级仪表盘的核心武器. 当面对多维数据集时,合理的子图布局能显著提升多数据关联分析效率,让数据的呈现更加直观和美观. 本文将深入探讨Plotly中子图布 ...
- C# 线程(四)——异步
参考: .NET进阶篇06-async异步.thread多线程3 - 知乎 (zhihu.com) C# Async/Await: 让你的程序变身时间管理大师_哔哩哔哩_bilibili C# 异步编 ...
- CSharp_core
C#核心篇 面向对象的概念 封装(类).继承,多态 类 基本概念 具有相同特征.相同行为.一类事物的抽象 类是对象的模板,可以通过类创建出对象 关键词class 类的申明 申明在nameplace语句 ...
- rider 跑不动了,快找车吧=vscode
我的笔记本跑rdier有点吃紧了,T440s; rider的慢速是我有点难以接受了,在开发效率和性能方面综合考虑,我考虑换上vscode了. 做.net core web开发完全够用了,也不用各种等待 ...