哈喽大家好,我是 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}$):
- 点 $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_{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);//找出点ydis=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]){//继续dfsif(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.作业
- B4016 树的直径,模板题,难度:$1/1$。
- P3304 [SDOI2013] 直径,难度:$3/5$。
- P4408 [NOI2003] 逃学的小孩,难度$4/5$
5.闲话
蒟蒻不才,膜拜大佬,如果文章有什么问题,请在评论区@我。