[c++算法] 万字详解树的直径算法

哈喽大家好,我是 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 xy,它们的距离称作 d i s x → y dis_{x \to y} disxy):

  1. x x x 在树的直径 u → v u \to v uv
  2. x x x 不在树的直径 u → v u \to v uv 中,但 x → y x \to y xy 这条路径与树的直径 u → v u \to v uv 有一部分重合。
  3. x x x 不在树的直径 u → v u \to v uv 中,且 x → y x \to y xy 这条路径与树的直径 u → v u \to v uv 完全不重合。

(温馨提示:下面的内容建议自己先推一遍,画棵树想想再看)

先来看情况 1 1 1,若点 x x x 在树的直径 u → v u \to v uv 中且点 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} disxy 必定会大于 d i s x → u dis_{x \to u} disxu d i s x → v dis_{x \to v} disxv,因为 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} disuv=disux+disxv,又因为 d i s x → v < d i s x → y dis_{x \to v} < dis_{x \to y} disxv<disxy,那么此时这棵树的直径便是 u → y u \to y uy 这两条路,与直径的定以不符,所以错误。

再来看情况 2 2 2,点 x x x 不在树的直径 u → v u \to v uv 中,但 x → y x \to y xy 这条路径与树的直径 u → v u \to v uv 有一部分重合。这里又可以分成两种情况。

  1. x → y x \to y xy 被完全包含在 u → v u \to v uv 内,这是显然不可能的。
  2. x → y x \to y xy 有一部分包含在 u → v u \to v uv 内,那我们可以设点 o o o 为公共部分其中的一个点,那么此时 d i s o → y dis_{o \to y} disoy 一定要大于 d i s o → v dis_{o \to v} disov d i s o → u dis_{o \to u} disou,与直径的定以不符,所以错误。

最后来看情况 3 3 3,点 x x x 不在树的直径 u → v u \to v uv 中,且 x → y x \to y xy 这条路径与树的直径 u → v u \to v uv 完全不重合。

这时,我们设点 o o o u → v u \to v uv 内,因为每棵树都是连通的,所以必定有一条 x → o x \to o xo 路。于是,就得到了一下式子:

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} disuv=disuo+disov=disuo+disxvdisxo

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} disuy=disuo+disoy=disuo+disxydisxo

将两个式子互相抵消,分别得到 d i s x → v dis_{x \to v} disxv d i s x → y dis_{x \to y} disxy,因为 d i s x → y > d i s x → v dis_{x \to y} > dis_{x \to v} disxy>disxv,所以得到 d i s u → v < d i s u → y dis_{u \to v} < dis_{u \to y} disuv<disuy,与直径的定以不符,所以错误。

至此,证毕。

于是!我们可以从点 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 56 这一条路,但是如果你从点 1 1 1 开始进行 DFS,只能找到点 3 3 3,因为中间被 2 → 4 2 \to 4 24 这条边挡住了,从 1 → 5 1 \to 5 15 不是最优解。

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+disrootvi)

其他节点也同理。

这时,有聪明的读者就会说了:你这不是只更新了它的一个子树吗,如果树的直径是这样子,那你的 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+disxvi)

至于为什么是 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+disxvi 表示这棵子树的最大的长度,所以, 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 x1 千米到第 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 1N104),输出每个节点 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(disui,disvi)

代码:

#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 赢。

那我们就可以分情况讨论了:

  1. Alice 一步就可以追上 Bob:这一看就是 Alice 赢啊。
  2. d a > d b da>db da>db 也就是 Alice 每次走的步数比 Bob 多,那么 Bob 肯定会被抓到,那么也是 Alice 赢。
  3. 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。

但是,绕着跑是有条件的:

  1. 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 db4 时,Bob 不论跑到哪里(点 b , 1 , a , 2 , 3 b,1,a,2,3 b,1,a,2,3),都会被 Alice 抓到。
  2. ⌈ l e n 2 ⌉ ≤ d a \lceil \frac{len}{2} \rceil \le da 2lenda这里的 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\} XY={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 disxy=cntx+cnty2,其中 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.作业

  1. B4016 树的直径,模板题,难度: 1 / 1 1/1 1/1
  2. P3304 [SDOI2013] 直径,难度: 3 / 5 3/5 3/5
  3. P4408 [NOI2003] 逃学的小孩,难度 4 / 5 4/5 4/5

5.闲话

蒟蒻不才,膜拜大佬,如果文章有什么问题,请在评论区@我。
什么你居然能看到这里,能不能点个赞啊QwQ \color{white} \tiny \texttt{什么你居然能看到这里,能不能点个赞啊QwQ} 什么你居然能看到这里,能不能点个赞啊QwQ

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值