[c++算法] 树的直径,包教包会!

哈喽大家好,我是 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.闲话

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

posted @ 2025-02-26 13:07  doooge  阅读(257)  评论(0)    收藏  举报