[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\) 数组里。因为要找到两个长度最大的长度,所以更新代码为这样:
至于为什么是 \(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.闲话
蒟蒻不才,膜拜大佬,如果文章有什么问题,请在评论区@我。

浙公网安备 33010602011771号