哈喽大家好,我是 doooge。今天我们要讲树论中的一个算法-树的直径。
树的直径详解 \Huge 树的直径 详解 树的直径详解
1.树的直径是什么
这是一棵图论中的树:
这棵树的直径就是这棵树中最长的一条简单路径。
2.树的直径怎么求
2.1暴力算法
直接对每个点进行 DFS,找到每个点离最远的点的距离,最后求出最长的一条路,也就是树的直径。
时间复杂度: O ( n 2 ) O(n^2) O(n2)
代码我就只放 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 x,离 x x x 最远的点一定是树的直径的一个顶点。
为什么呢?
我们可以用反证法来推导:
假设树的直径的端点为 u u u 和 v v v,设对于每一个离点 x x x 最远的点 y y y 不是树的直径的端点 u u u 和 v v v,按我们可以分类讨论(以下把点 x x x 到点 y y y 的路径称作 x → y x \to y x→y,它们的距离称作 d i s x → y dis_{x \to y} disx→y):
- 点 x x x 在树的直径 u → v u \to v u→v 中
- 点 x x x 不在树的直径 u → v u \to v u→v 中,但 x → y x \to y x→y 这条路径与树的直径 u → v u \to v u→v 有一部分重合。
- 点 x x x 不在树的直径 u → v u \to v u→v 中,且 x → y x \to y x→y 这条路径与树的直径 u → v u \to v u→v 完全不重合。
(温馨提示:下面的内容建议自己先推一遍,画棵树想想再看)
先来看情况 1 1 1,若点 x x x 在树的直径 u → v u \to v u→v 中且点 y y y 既不等于 u u u 也不等于 v v v。
因为 y y y 既不等于 u u u 也不等于 v v v,那么 d i s x → y dis_{x \to y} disx→y 必定会大于 d i s x → u dis_{x \to u} disx→u 和 d i s x → v dis_{x \to v} disx→v,因为 d i s u → v = d i s u → x + d i s x → v dis_{u \to v} = dis_{u \to x} + dis_{x \to v} disu→v=disu→x+disx→v,又因为 d i s x → v < d i s x → y dis_{x \to v} < dis_{x \to y} disx→v<disx→y,那么此时这棵树的直径便是 u → y u \to y u→y 这两条路,与直径的定以不符,所以错误。
再来看情况 2 2 2,点 x x x 不在树的直径 u → v u \to v u→v 中,但 x → y x \to y x→y 这条路径与树的直径 u → v u \to v u→v 有一部分重合。这里又可以分成两种情况。
- x → y x \to y x→y 被完全包含在 u → v u \to v u→v 内,这是显然不可能的。
- x → y x \to y x→y 有一部分包含在 u → v u \to v u→v 内,那我们可以设点 o o o 为公共部分其中的一个点,那么此时 d i s o → y dis_{o \to y} diso→y 一定要大于 d i s o → v dis_{o \to v} diso→v 和 d i s o → u dis_{o \to u} diso→u,与直径的定以不符,所以错误。
最后来看情况 3 3 3,点 x x x 不在树的直径 u → v u \to v u→v 中,且 x → y x \to y x→y 这条路径与树的直径 u → v u \to v u→v 完全不重合。
这时,我们设点 o o o 于 u → v u \to v u→v 内,因为每棵树都是连通的,所以必定有一条 x → o x \to o x→o 路。于是,就得到了一下式子:
d i s u → v = d i s u → o + d i s o → v = d i s u → o + d i s x → v − d i s x → 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} disu→v=disu→o+diso→v=disu→o+disx→v−disx→o
d i s u → y = d i s u → o + d i s o → y = d i s u → o + d i s x → y − d i s x → 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} disu→y=disu→o+diso→y=disu→o+disx→y−disx→o
将两个式子互相抵消,分别得到 d i s x → v dis_{x \to v} disx→v 和 d i s x → y dis_{x \to y} disx→y,因为 d i s x → y > d i s x → v dis_{x \to y} > dis_{x \to v} disx→y>disx→v,所以得到 d i s u → v < d i s u → y dis_{u \to v} < dis_{u \to y} disu→v<disu→y,与直径的定以不符,所以错误。
至此,证毕。
于是!我们可以从点 1 1 1 开始 DFS,找到离点 1 1 1 最远的点 y y y,再进行 DFS 找到离点 y y 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;
}
该模版写法不一,也可以用 d i s dis dis 数组存储距离,DFS 完后再找最大的路径。该带码也同样适用于带边权的树。
时间复杂度: O ( n ) O(n) O(n)。
注意:该算法只能在所有边权为正数的情况下成立,否则会出问题,具体为什么下面会讲。
我们来看这张图:
不难发现,这棵树的直径是 5 → 6 5 \to 6 5→6 这一条路,但是如果你从点 1 1 1 开始进行 DFS,只能找到点 3 3 3,因为中间被 2 → 4 2 \to 4 2→4 这条边挡住了,从 1 → 5 1 \to 5 1→5 不是最优解。
2.3 方法3:树形DP
主播主播,你的 DFS 大法确实很强,但是还是太吃条件了,有没有既速度快又没有限制的算法呢?
有的兄弟,有的,像这样的算法,主播还有一个,都是 T0.5 的强势算法,只要你掌握一个,就能秒杀 CSP 树的直径,如果想主播这样两个都会的话,随便进 NOI CSP-S。
好了,回归正题,我们来讲讲树形DP 的写法。 d p x dp_x dpx 表示从 x x x 出发走向 x x x 的子树中最长的一条路径。
2.3.1 思路1:
假设有一棵树的根节点为 r o o t root root(我们这里称把 x x x 的子节点称作为 x i x_i xi),那么我们的 d p r o o t dp_{root} dproot 就表示从 r o o t root root 节点出发能走到的最远距离,也就是 r o o t root root 的子树的最大的深度。所以,我们得要从子树开始更新,也就是在这里:
for(auto i:v[x]){//继续dfs
if(i.x==fa)continue;//不能走回头路
dfs(i.x);//往下搜索
dp[x]=...;//这里开始更新,此时先dfs的子节点会先更新dp
}
那么,我们就可以在遍历子节点 v i v_i vi 的时候更行新 d p r o o t dp_{root} dproot:
d p r o o t = max ( d p r o o t , d p v i + d i s r o o t → v i ) dp_{root}=\max(dp_{root},dp_{v_i}+dis_{root \to v_i}) dproot=max(dproot,dpvi+disroot→vi)
其他节点也同理。
这时,有聪明的读者就会说了:你这不是只更新了它的一个子树吗,如果树的直径是这样子,那你的 DP 不是就错了吗?
读者说的没错,我们要考虑图片上的情况。
我们可以设置一个中间节点,比如这张图的中间点就是 r o o t root root 节点,一条路径可以贯穿一个中间节点的两个子树,而我们的 d p dp dp 数组只记录了一个子树的最大的深度,也就是子树的最长路。
于是,我们可以在更新 d p dp dp 数组的时候同时更新另一个变量 a n s x ans_x ansx,表示若 x x x 为树的直径的中间点,穿过 x x x 最长的路径的长度。当然, d p dp dp 数组也不能落下,但是答案还是存在 a n s ans ans 数组里。因为要找到两个长度最大的长度,所以更新代码为这样:
a n s x = max ( a n s x , d p x + d p v i + d i s x → v i ) ans_x=\max(ans_x,dp_x+dp_{v_i}+dis_{x \to v_i}) ansx=max(ansx,dpx+dpvi+disx→vi)
至于为什么是 d p x + d p v i dp_x+dp_{v_i} dpx+dpvi 因为此时的 d p x dp_x dpx 表示的是在 v i v_i vi 之前遍历到的子树的最大值, d p v i + d i s x → v i dp_{v_i}+dis_{x \to v_i} dpvi+disx→vi 表示这棵子树的最大的长度,所以, a n s ans ans 数组的更新应该在 d p dp 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);
}
}
2.3.2 重点:思路2
不难发现,一棵树的直径一定是由某一个节点 x x x 和两条或一条与 x x x 相连的路径所组成的。
怎么证明呢?我们一样可以用反证法:
如果树的直径由 某个节点 x x x 和两条以上的路径组成,那么说明至少要有一条从点 x x x 通往某一个点再从一条与上面两条路完全不重合的路回来,就像图中这样:
图中绿色的路线为树的直径。
不难发现,每个点和任意点之间只有一条唯一的路径,也就是说,点 y y y 到点 x x x 只有一条路径,所以如果树的直径由 某个节点 x x x 和两条以上的路径组成的猜想不成立,证毕。
所以,树的直径必定在距离每个点 x x x 的最长的路径和次长的路径之中,我们可以维护一个数组 d p dp dp 和 d p 2 dp2 dp2, d p i dp_i dpi 和 d p 2 i dp2_i dp2i 分别表示距离 i i i 最长和次长的路径的长度,经过 i i i 的答案就是 d p i + d p 2 i dp_i+dp2_i dpi+dp2i,最后遍历求 max ( d p i ) \max(dp_i) max(dpi) 找答案。
代码很好写,就不推 d p dp dp 和 d p 2 dp2 dp2 的数组的转移了,代码:
#include<bits/stdc++.h>
using namespace std;
int dp[100010],dp2[100010];
vector<int>v[100010];
void dfs(int x,int fa){
// dp[x]=dp2[x]=-1e9;如果有负边权就需要初始化,但是叶节点的dp要初始化成0,具体代码看下面。
// bool flag=false;判断该节点是不是叶节点,如果能向下遍历就不是叶节点
for(auto i:v[x]){
if(i==fa)continue;//不能走回头路
// flag=true;//如果可以继续向下搜索就不是叶节点
dfs(i,x);//继续搜索
if(dp[i]+1>dp[x]){//注意这里如果有边权要加上边权
//如果从这条路走的长度比dp[x]要长
dp2[x]=dp[x];//更新,把dp[x]传下去给dp2[x]
dp[x]=dp[i]+1;//更新dp[x]
}
else if(dp[i]+1>dp2[x]){
//如果从这条路走的长度比dp2[x]要长
dp2[x]=dp[i]+1;//直接更新dp2[x]
}
}
//if(!flag)dp[i]=dp2[i]=0;如果是叶节点就要初始化成0
return;
}
int main(){
int n,ans=0;//ans用于最后求max,所以如果有负边权就要初始化成-1e9
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);//从什么点遍历都可以
for(int i=1;i<=n;i++){//注意最后要遍历找答案
ans=max(ans,dp[i]+dp2[i]);//dp[i]+dp2[i]就是经过点i的最长的路
}
cout<<ans<<endl;
return 0;
}
3.例题
3.1 T1.P8602 [蓝桥杯 2013 省 A] 大臣的旅费
3.1.1 题目描述
很久以前,T 王国空前繁荣。为了更好地管理国家,王国修建了大量的快速路,用于连接首都和王国内的各大城市。
为节省经费,T 国的大臣们经过思考,制定了一套优秀的修建方案,使得任何一个大城市都能从首都直接或者通过其他大城市间接到达。同时,如果不重复经过大城市,从首都到达每个大城市的方案都是唯一的。
J 是 T 国重要大臣,他巡查于各大城市之间,体察民情。所以,从一个城市马不停蹄地到另一个城市成了 J 最常做的事情。他有一个钱袋,用于存放往来城市间的路费。
聪明的 J 发现,如果不在某个城市停下来修整,在连续行进过程中,他所花的路费与他已走过的距离有关,在走第 x − 1 x - 1 x−1 千米到第 x x x 千米这一千米中( x x x 是整数),他花费的路费是 x + 10 x+10 x+10 这么多。也就是说走 1 1 1 千米花费 11 11 11,走 2 2 2 千米要花费 23 23 23。
J 大臣想知道:他从某一个城市出发,中间不休息,到达另一个城市,所有可能花费的路费中最多是多少呢?
(绝对不是水字数)
3.1.2 思路+代码
这道题乍一看上去确实很乱,但我们可以找找关键句(跟语文课上学的一样)。
如果不重复经过大城市,从首都到达每个大城市的方案都是唯一的。
咦?这句话的意思不就是从根节点出发到每一个节点的路径唯一吗?
他从某一个城市出发,中间不休息,到达另一个城市,所有可能花费的路费中最多是多少呢
咦?这句话不就是要求一棵树上最长的一条路径吗?
综上所述,这道题完完全全就是树的直径的板子,只是读题困难一点而已。需要注意,最后的答案并不是树的直径的长度,而是像题目描述中的这样:
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 1/5 1/5。
3.2 T2.HDU 2196 Computer
请注意,这道题不是洛谷的,需要在 vjudge 上交代码。
3.2.1 题目描述
给定一棵节点为 N N N 的树( 1 ≤ N ≤ 1 0 4 1 \le N \le 10^4 1≤N≤104),输出每个节点 i i i 离 i i i 最远的节点的长度。
3.2.2 思路+代码
首先, O ( N 2 ) O(N^2) O(N2) 的暴力 DFS 是不可能的,因为题目中还有 T T T 组数据。想一想,对于每个节点 i i i 离 i i i 最远的点是什么呢?
对的,之前说过,就是树的直径的两个端点!所以离每一个节点 i i i 最远的点就是树的直径的两端的节点 u u u 和 v v v。
于是,我们可以用 O ( N ) O(N) O(N) 的 DFS 先将树的直径的两个端点求出来,在继续用 O ( N ) O(N) O(N) 的 DFS 求出对每个节点的距离,对于节点 i i i,它的答案就是:
max ( d i s u → i , d i s v → i ) \max(dis_{u \to i},dis_{v \to i}) max(disu→i,disv→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 3/5 3/5。
3.3 T3:CF734E Anton and Tree
3.3.1 题目描述
给一棵 n ( n < = 200000 ) n(n<=200000) n(n<=200000) 个节点的树,每个点为黑色或白色,一次操作可以使一个相同颜色的连通块变成另一种颜色,求使整棵树变成一种颜色的最少操作数。
3.3.2 思路+代码
首先我们看个样例:
输入:
11
0 0 0 1 1 0 1 0 0 1 1
1 2
1 3
2 4
2 5
5 6
5 7
3 8
3 9
3 10
9 11
输出:
2
解释一下:
这是一开始树的样子。我们可以将节点 2 2 2 和周围的白色节点染成黑色,于是图中的树变成了这样:
再将点 6 6 6 染色即可
我们可以将它转成一个连通块的问题,我们将一些相邻的颜色相同的点划为一个连通块,再将每一个连通块缩成一个点,再求树的直径。
当然,缩点可以用并查集来做,但是我们同样可以将连通块中间的点与点之间的边权设为0。
代码:
#include<bits/stdc++.h>
using namespace std;
int a[200010],dp[200010],dp2[200010];
struct ll{
int x,w;
};
vector<int>v[200010];
vector<ll>g[200010];
void dfs(int x,int fa){
if(a[x]==a[fa]){
g[fa].push_back({x,0});
g[x].push_back({fa,0});
}
else{
g[fa].push_back({x,1});
g[x].push_back({fa,1});
}
for(auto i:v[x]){
if(i==fa)continue;
dfs(i,x);
}
}
void dfs_dp(int x,int fa){
for(auto i:g[x]){
if(i.x==fa)continue;
dfs_dp(i.x,x);
if(dp[i.x]+i.w>dp[x]){
dp2[x]=dp[x];
dp[x]=dp[i.x]+i.w;
}
else if(dp[i.x]+i.w>dp2[x]){
dp2[x]=dp[i.x]+i.w;
}
}
return;
}
int main(){
int n;
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
for(int i=1;i<n;i++){
int x,y;
cin>>x>>y;
v[x].push_back(y);
v[y].push_back(x);
}
a[0]=-1;
dfs(1,0);
dfs_dp(1,0);
int ans=0;
for(int i=1;i<=n;i++){
ans=max(ans,dp[i]+dp2[i]);
}
cout<<((ans+1)>>1)<<endl;
return 0;
}
难度: 3 / 5 3/5 3/5。
3.4 T4.CF1404B Tree Tag
3.4.1 题目描述
有一棵 n 个点的树, Alice 和 Bob 初始在这棵树上的节点 a, b。
他们可以在树上轮流移动一段距离不超过 da 和 db 的路径。
路径长度的定义是两点之间树上简单路径的边数。
如果 Alice 能在 1 0 100 10^{100} 10100 次内追到 Bob ,那么则算 Alice 赢,否则算 Bob 赢。
3.4.2 思路+代码
哇,这题不会是博弈论吧?
其实不然,这题只是借用了博弈论的思想而已。
首先,题目中的 1 0 100 10^{100} 10100 显然就不是给人模拟的,也就是,如果 Alice 如果有追上 Bob 的方法,那么必然是 Alice 赢。
那我们就可以分情况讨论了:
- Alice 一步就可以追上 Bob:这一看就是 Alice 赢啊。
- d a > d b da>db da>db 也就是 Alice 每次走的步数比 Bob 多,那么 Bob 肯定会被抓到,那么也是 Alice 赢。
- d a < d b da<db da<db 这个情况就有点复杂了。
我们知道,Alice 身在点 ,而 Bob 身在点 B,我们总是可以画出这样一棵树来:
在这样一棵树中,不是点 a a a 总是树的根节点,而点 b b b 总是在这棵树其中的一棵子树中的一个叶节点里。此时,Bob 只能向深处逃离 Alice。
那么,有没有一种可能,Bob 可以这样子绕着 Alice 逃离呢?比如说这张图(假设此时 d a = 1 , d b = 3 da=1,db=3 da=1,db=3):
Alice 此时离 Bob 只有一步之遥,但是 Bob 却可以走到点 2 2 2:
此时 Alice 又要赶到点 1 1 1,然而 Bob 却又可以绕到了点 3 3 3,就这样,Bob 可以无限绕着树的直径来跑,而 Alice 永远都跟不上 Bob。
但是,绕着跑是有条件的:
-
d
a
>
d
b
×
2
da>db \times 2
da>db×2,如果
d
a
<
d
b
×
2
da<db \times 2
da<db×2,那么就一定会发生这样的情况:
。若此时的 d a = 2 da=2 da=2, d b ≤ 4 db \le 4 db≤4 时,Bob 不论跑到哪里(点 b , 1 , a , 2 , 3 b,1,a,2,3 b,1,a,2,3),都会被 Alice 抓到。
-
⌈
l
e
n
2
⌉
≤
d
a
\lceil \frac{len}{2} \rceil \le da
⌈2len⌉≤da,这里的
l
e
n
len
len 表示树的直径的长度,此时 Bob 就算想跑也会被限制,如图:
。若此时 d a = 2 da=2 da=2, d b db db 不管多大,不管 Bob 能跑到哪里,都会被 Alice 抓住。
除了这两个情况之外,剩下就是 Bob 赢啦。
总结一下,我们可以先求出树的直径的长度,然后根据上面的条件来判断即可。
代码:
#include<bits/stdc++.h>
using namespace std;
int dp[100010],dp2[100010],dis[100010];
vector<int>v[100010];
void dfs(int x,int fa,int sum){//第一次dfs求每个点到点a的路径长度
dis[x]=sum;
for(auto i:v[x]){
if(i==fa)continue;
dfs(i,x,sum+1);
}
return;
}
void dfs_dp(int x,int fa){//第二次dfs求出树的直径
for(auto i:v[x]){
if(i==fa)continue;
dfs_dp(i,x);
if(dp[i]+1>dp[x]){
dp2[x]=dp[x];
dp[x]=dp[i]+1;
}
else if(dp[i]+1>dp2[x]){
dp2[x]=dp[i]+1;
}
}
return;
}
int main(){
int T;
cin>>T;
while(T--){//注意有多组测试数据
int n,x,y,dx,dy;
cin>>n>>x>>y>>dx>>dy;
for(int i=1;i<=n;i++){
dp[i]=dp2[i]=dis[i]=0;
v[i].clear();//注意初始化
}
for(int i=1;i<n;i++){
int x,y;
cin>>x>>y;
v[x].push_back(y);
v[y].push_back(x);
}
dfs(x,-1,0);
dfs_dp(1,-1);
int ans=0;
for(int i=1;i<=n;i++){
ans=max(ans,dp[i]+dp2[i]);
}
if(dx>=dis[y]/*第一回直接被抓住*/||dy<=2*dx||(ans+1)/2<=dx){//db<da可以和db<=da*2合并
cout<<"Alice\n";
}
else{
cout<<"Bob\n";
}
}
return 0;
}
难度: 4 / 5 4/5 4/5,感觉这个难度可以放在 CSP-S 的第二题。
3.5 T5.P3174 [HAOI2009] 毛毛虫
3.5.1 题目描述
对于一棵树,我们可以将某条链和与该链相连的边抽出来,看上去就象成一个毛毛虫,点数越多,毛毛虫就越大。例如下图左边的树(图 1)抽出一部分就变成了右边的一个毛毛虫了(图 2)。
给出一棵树,输出最大的毛毛虫的大小。
3.5.2 思路+代码:
乍一看,这道题似乎就是一个树的直径模版题,但是这题还是有点复杂的。我们知道,影响树的直径很重要的一个因素就是边权,而这道题怎么才能求出边权呢?
诶!我有一计!
考虑一下毛毛虫只有什么组成的。不难发现,它其实是由一条路径和路径上每一个点相邻的点组成的。那么!我们可以求出每个点和它相邻的点的个数(包括自己),看做自己的点权!
这时候有读者说了:这不就是拓扑排序里的吗?
的确如此,在建边的时候,我们给每条边的两个端点更新点权,就像拓扑排序中计算 in
数组一样。
代码:
int x,y;
cin>>x>>y;
v[x].push_back(y);
v[y].push_back(x);//v用来存图
cnt[x]++,cnt[y]++;//cnt计数
注意:因为要算上自己,所以 c n t cnt cnt 数组要初始化为 1 1 1!
那么我们如何才能将点权转化为边权呢?
画棵树试试。
如图,点 1 1 1 和点 2 2 2 的点权分别是 3 3 3 和 4 4 4。因为和点 1 1 1 相连的点有 1 , 2 , 3 1,2,3 1,2,3,和点 2 2 2 相连的点有 1 , 2 , 4 , 5 1,2,4,5 1,2,4,5(包括自己)。如果我们选择点 1 1 1 和点 2 2 2,那么我们选择的点组成的毛毛虫的大小就是 5 5 5,包括了点 1 , 2 , 3 , 4 , 5 1,2,3,4,5 1,2,3,4,5 这五个点,也就是图中标黑的这些点。
这时,聪明的读者肯定会发现,点 1 1 1 和点 2 2 2 的点权之和为 7 7 7,比 5 5 5 多了 2 2 2。为什么啊?我们可以把点 1 1 1 和点 2 2 2 每个点和它相邻的点列出来,就像:
{ 1 , 2 , 3 } ∪ { 1 , 2 , 4 , 5 } = { 1 , 2 , 3 , 4 , 5 } \{1,2,3\} \cup \{1,2,4,5\} = \{1,2,3,4,5\} {1,2,3}∪{1,2,4,5}={1,2,3,4,5}
我们会发现,点 1 1 1 和点 2 2 2 在两个集合中都出现过,所以它们重合了,去重后便是我们想要的答案,重合的部分可以这样表示:
{ 1 , 2 , 3 } ∩ { 1 , 2 , 4 , 5 } = { 1 , 2 } \{1,2,3\} \cap \{1,2,4,5\} = \{1,2\} {1,2,3}∩{1,2,4,5}={1,2}
诶!我好想发现了!如果选点 x x x 和点 y y y,那么它们相邻的点的集合必定都包含 x x x 和 y y y,其它的都不重合!就像:
X ∩ Y = { x , y } X \cap Y = \{x,y\} X∩Y={x,y}
其中 X X X 表示点 x x x 和其它相邻的点, Y Y Y 是点 y y y 和其它相邻的点。
于是,我们的边权就出来了, d i s x → y = c n t x + c n t y − 2 dis_{x \to y} = cnt_x+cnt_y-2 disx→y=cntx+cnty−2,其中 c n t x cnt_x cntx 表示 x x x 的点权, c n t y cnt_y cnty 表示 y y y 的点权。
既然边权已经出来了,那么代码也就呼之欲出了!
代码:
#include<bits/stdc++.h>
using namespace std;
int dis,pos;
vector<int>v[300010];
int cnt[300010];
bool f[300010];
void dfs(int x,int fa,int sum){
f[x]=true;
if(sum>=dis){
dis=sum;
pos=x;
}
for(auto i:v[x]){
if(i==fa)continue;
dfs(i,x,sum+cnt[i]-2);
}
return;
}
int main(){
int n,m;
cin>>n>>m;
fill(cnt+1,cnt+n+1,1);//注意初始化为1!
for(int i=1;i<n;i++){
int x,y;
cin>>x>>y;
v[x].push_back(y);
v[y].push_back(x);
cnt[x]++,cnt[y]++;
}
dfs(1,-1,cnt[1]);
dis=0;
dfs(pos,-1,cnt[pos]);
cout<<dis<<endl;
return 0;
}
难度: 4 / 5 4/5 4/5,小声说,这道题以前可是蓝题呢~
4.作业
- B4016 树的直径,模板题,难度: 1 / 1 1/1 1/1。
- P3304 [SDOI2013] 直径,难度: 3 / 5 3/5 3/5。
- P4408 [NOI2003] 逃学的小孩,难度 4 / 5 4/5 4/5
5.闲话
蒟蒻不才,膜拜大佬,如果文章有什么问题,请在评论区@我。
什么你居然能看到这里,能不能点个赞啊QwQ
\color{white} \tiny \texttt{什么你居然能看到这里,能不能点个赞啊QwQ}
什么你居然能看到这里,能不能点个赞啊QwQ