树的直径的常见结论和技巧及其应用

本文通过分析经典图论问题——树的直径,讲解如何利用DFS寻找树的直径及其端点,并以此解决相关问题。文章通过实例详细阐述了如何在给定树结构中找到距离任一节点最远的节点,以及如何找到使得树中某些节点出现魔鬼的最小条件。同时,文章还介绍了如何找到树中三个点,使得它们之间的路径包含的边数最多。最后,文章提供了一道关于树的直径和边数量的问题,解释了如何在求解过程中避免超时错误。
摘要由CSDN通过智能技术生成

这次我主要是想通过经典习题的分析来向大家说明树的直径中一些重要结论与技巧。

先给出一道题:

A school bought the first computer some time ago(so this computer's id is 1). During the recent years the school bought N-1 new computers. Each new computer was connected to one of settled earlier. Managers of school are anxious about slow functioning of the net and want to know the maximum distance Si for which i-th computer needs to send signal (i.e. length of cable to the most distant computer). You need to provide this information.



Hint: the example input is corresponding to this graph. And from the graph, you can see that the computer 4 is farthest one from 1, so S1 = 3. Computer 4 and 5 are the farthest ones from 2, so S2 = 2. Computer 5 is the farthest one from 3, so S3 = 3. we also get S4 = 4, S5 = 4.

Input

Input file contains multiple test cases.In each case there is natural number N (N<=10000) in the first line, followed by (N-1) lines with descriptions of computers. i-th line contains two natural numbers - number of computer, to which i-th computer is connected and length of cable used for connection. Total length of cable does not exceed 10^9. Numbers in lines of input are separated by a space.

Output

For each case output N lines. i-th line must contain number Si for i-th computer (1<=i<=N).

Sample Input

5
1 1
2 1
3 1
1 1

Sample Output

3
2
3
4
4

题意就是说给出点以及点与点之间的边,让你求出来距离图中所有的点的最大距离。

猛一看这道题可能比较复杂,我们不可能是把每一个点都搜索一次来找距离该点的最大距离,这样必然会超时,所以我们需要仔细分析一下。还记得开头我说的怎么用dfs求树的直径了吗,一开始随意找个点搜索一次,找出距离该点最大的点,这个点就是直径的一个端点,那我们反过来思考,是不是距离每个点的最大距离一定都是直径的端点呢,那这是不是意味着我们只要先找到直径的两个端点,然后用直径的两个端点进行搜索,对每个点的距离取一个最大值就可以找到最优解呢?思路大概就是这样了,下面我给出代码。

#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
const int N=25003;
int h[N],e[N],ne[N],idx,w[N],d1[N],d2[N];
bool vis[N];
void add(int x,int y,int z)//链式向前星存树 
{
	e[idx]=y;
	ne[idx]=h[x];
	w[idx]=z;
	h[x]=idx++;
}
void dfs(int x,int father)
{
	for(int i=h[x];i!=-1;i=ne[i])
	{
		int j=e[i];
		if(j==father) continue;//防止原路返回 
		d1[j]=d1[x]+w[i];//更新距离初始点距离 
		dfs(j,x);//搜索下一个节点 
	}
}
int main()
{
	int n;
	while(scanf("%d",&n)!=EOF)
	{
		memset(h,-1,sizeof h);
		idx=0;
		int j,w;
		for(int i=2;i<=n;i++)
		{
			scanf("%d%d",&j,&w);
			add(i,j,w);
			add(j,i,w);
		}
		memset(d1,0,sizeof d1);
		memset(d2,0,sizeof d2);
		dfs(1,0);
		int ans=0,e1,e2;
		for(int i=1;i<=n;i++)//找到直径的第一个端点 
		if(d1[i]>ans)
		{
			ans=d1[i];
			e1=i;
		}
		memset(d1,0,sizeof d1);
		memset(vis,false,sizeof vis);
		dfs(e1,0);//用直径的第一个端点搜索来找第二个端点,顺便求出第一个端点距离树中其他点的距离 
		ans=0;
		for(int i=1;i<=n;i++)//找到直径的第二个端点
		{
			if(d1[i]>ans)
			{
				ans=d1[i];
				e2=i;
			}
			d2[i]=d1[i];
		}
		memset(d1,0,sizeof d1);
		dfs(e2,0);// 求出第二个端点距离树中其他点的距离 
		for(int i=1;i<=n;i++)
			printf("%d\n",max(d1[i],d2[i]));//每个点取距离直径两个端点的最大距离即可得到答案 
	}
	return 0;
}

补充一点,这道题还可以用树形dp做,我将会在介绍树形dp常见搜索方式中给出分析。

下面再给出一道例题:

给你一颗N个节点的树,已知我们在一个节点u放置一本魔法书的话,距离节点u小于等于d的节点就会出现魔鬼。

现在已知有m个点肯定有魔鬼,问哪些点可以放置魔法书。

Input

The first line contains three space-separated integers nm and d (1 ≤ m ≤ n ≤ 100000; 0 ≤ d ≤ n - 1). The second line contains m distinct space-separated integers p1, p2, ..., pm (1 ≤ pi ≤ n). Then n - 1 lines follow, each line describes a path made in the area. A path is described by a pair of space-separated integers ai and bi representing the ends of this path.

Output

Print a single number — the number of settlements that may contain the Book of Evil. It is possible that Manao received some controversial information and there is no settlement that may contain the Book. In such case, print 0.

Examples

Input

6 2 3
1 2
1 5
2 3
3 4
4 5
5 6

Output

3

先给出一个结论吧:

在一棵有n个节点的树中选取m个节点用原有边组成子树,求出这m个点形成的子树的直径,则m个点中距离图中任意一点的最大距离的点一定是子树中直径的两个端点之一。

有了这个结论这道题也就不难想了,只需先找出子树的直径,然后用直径的两个端点去遍历整张图,找距离直径两个端点的最大值小于d的点即可,下面上代码:

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
const int N=2e5+10;
bool vis[N];
int d[N],h[N],ne[N],e[N],idx,dp[N];
void add(int x,int y)
{
	e[idx]=y;
	ne[idx]=h[x];
	h[x]=idx++;
}
void dfs(int x,int father)
{
	for(int i=h[x];i!=-1;i=ne[i])
	{
		int j=e[i];
		if(j==father) continue;
		d[j]=d[x]+1;
		dfs(j,x);
	}
}
int main()
{
	int n,m,p,t;
	cin>>n>>m>>p;
	memset(h,-1,sizeof h);
	for(int i=1;i<=m;i++)
	{
		scanf("%d",&t);
		vis[t]=true;
	}
	for(int i=1;i<n;i++)
	{
		int u,v;
		scanf("%d%d",&u,&v);
		add(u,v);add(v,u);
	}
	dfs(1,0);//找直径的一个端点 
	int ans=0,e1,e2;
	for(int i=1;i<=n;i++)
	{
		if(d[i]>ans&&vis[i])
		{
			ans=d[i];
			e1=i;
		}
	}
	d[e1]=0;
	dfs(e1,0);//找直径的另一个端点,顺便用一个端点遍历一次整棵树 
	ans=0;
	for(int i=1;i<=n;i++)
		if(d[i]>ans&&vis[i])
		{
			ans=d[i];
			e2=i;
		}
	memcpy(dp,d,sizeof dp);
	d[e2]=0;
	dfs(e2,0);//用另一个端点遍历一次整棵树
	ans=0;
	for(int i=1;i<=n;i++)
		if(max(dp[i],d[i])<=p)
			ans++;
	printf("%d",ans);
	return 0;
}

下面再来一道题

 You are given an unweighted tree with nn vertices. Recall that a tree is a connected undirected graph without cycles.

Your task is to choose three distinct vertices a, b, ca,b,c on this tree such that the number of edges which belong to at least one of the simple paths between aa and bb , bb and cc , or aa and cc is the maximum possible. See the notes section for a better understanding.

The simple path is the path that visits each vertex at most once.

输入格式

The first line contains one integer number nn ( 3 \le n \le 2 \cdot 10^53≤n≤2⋅105 ) — the number of vertices in the tree.

Next n - 1n−1 lines describe the edges of the tree in form a_i, b_iai​,bi​ ( 1 \le a_i1≤ai​ , b_i \le nbi​≤n , a_i \ne b_iai​=bi​ ). It is guaranteed that given graph is a tree.

输出格式

In the first line print one integer resres — the maximum number of edges which belong to at least one of the simple paths between aa and bb , bb and cc , or aa and cc .

In the second line print three integers a, b, ca,b,c such that 1 \le a, b, c \le n1≤a,b,c≤n and a \ne, b \ne c, a \ne ca=,b=c,a=c .

If there are several answers, you can print any.

输入输出样例

输入 

8
1 2
2 3
3 4
4 5
4 6
3 7
3 8

输出

5
1 8 6

题目让求出来中间夹杂边数最多的三个点,下面给出想法,首先三个点中一定包含直径,我们的目的就是找到剩余的那个点,假设u,v是直径的两个端点,t是另一个点,那么我们则所求的路径长度就等于(   d( u , t )  + d( v , t ) + d( u , v )  )  /2;

我们可以用直径的两个端点去遍历整张图,选取到直径两端点的距离和最大的那个点就可以,思路就是这样,先证明结论的正确性:

我们的目的在于找三个点,使得三个点之间两两距离和的值达到最大,我们先用一个点e0去搜索,得到e1,不难知道e1为直径的一个端点,再用e1去搜索得到直径的另一个端点e2,由直径的定义我们可以知道图中最远的距离就是直径了,所以我们已经实现e0,e1之间的距离最大和e1和e2之间的距离最大,现在我们只要证明e0和e3的距离也达到最大值就行,同样是利用反证法证明,下面给出证明:

不妨假设距离e0第二远的点是e3

第一种情况:e0与e3与直径e1,e2不相交,设直径上的o点与e0,e3路径上的e4点相交

则d(e0,e4)+ d(e4,e3)>= d(e0,e4)+ d(e4,o)+d(o,e2)

等价于 d(e4,e3)>= d(e4,o)+d(o,e2)

则d(e1,o)+  d(o,e4)+ d(e4,e3)>=d(e1,o)+ d(o,e2)

这与e1,e2是直径的两个端点矛盾,故不成立
 

第二种情况:e0与e3与直径相交,不妨设交点为o

则d(e0,o)+d(o,e3)>= d(e0,o)+d(o,e2),等价于

d(o,e3)>= d(o,e2)则 d(e1,o)+d(o,e3)>= d(e1,o)+d(o,e2)

 这与e1,e2是直径的两个端点矛盾,故不成立

证毕!

下面是代码:

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
const int N=2e6+10;
int d[N],h[N],ne[N],e[N],idx,dp[N];
void add(int x,int y)
{
	e[idx]=y;
	ne[idx]=h[x];
	h[x]=idx++;
}
void dfs(int x,int father)
{
	for(int i=h[x];i!=-1;i=ne[i])
	{
		int j=e[i];
		if(j==father) continue;
		d[j]=d[x]+1;
		dfs(j,x);
	}
}
int main()
{
	int n;
	cin>>n;
	memset(h,-1,sizeof h);
	for(int i=1;i<n;i++)
	{
		int u,v;
		cin>>u>>v;
		add(u,v);add(v,u);
	}
	dfs(1,0);//找出直径的一个端点 
	int ans=0,e1,e2,e3;
	for(int i=1;i<=n;i++)
	{
		if(d[i]>ans)
		{
			ans=d[i];
			e1=i;
		}
	}
	memset(d,0,sizeof d);
	dfs(e1,0);//找出直径的另一个端点 
	ans=0;
	for(int i=1;i<=n;i++)
		if(d[i]>ans)
		{
			ans=d[i];
			e2=i;
		}
	memcpy(dp,d,sizeof dp);
	memset(d,0,sizeof d);
	dfs(e2,0);//用直径端点遍历整张图 
	ans=0;
	for(int i=1;i<=n;i++) ans=max(ans,d[i]);//找出直径长度 
	int anst=0;
	for(int i=1;i<=n;i++)//找出直径外一点到直径两端点的距离和的最大值并记录位置 
		if(dp[i]+d[i]>=anst&&i!=e1&&i!=e2)//确保第三个点不与直径两端点重合 
			{
				anst=dp[i]+d[i];
				e3=i;
			}
	printf("%d\n",(anst+ans)/2);
	printf("%d %d %d",e1,e2,e3);
	return 0;
}

最后,我再给出一道难度稍大的题:

题目描述

小Q最近学习了一些图论知识。根据课本,有如下定义。树:无回路且连通的无向图,每条边都有正整数的权值来表示其长度。如果一棵树有N个节点,可以证明其有且仅有N-1 条边。

路径:一棵树上,任意两个节点之间最多有一条简单路径。我们用 dis(a,b)表示点a和点b的路径上各边长度之和。称dis(a,b)为a、b两个节点间的距离。

直径:一棵树上,最长的路径为树的直径。树的直径可能不是唯一的。

现在小Q想知道,对于给定的一棵树,其直径的长度是多少,以及有多少条边满足所有的直径都经过该边。

输入格式

第一行包含一个整数N,表示节点数。 接下来N-1行,每行三个整数a, b, c ,表示点 a和点b之间有一条长度为c的无向边。

输出格式

共两行。第一行一个整数,表示直径的长度。第二行一个整数,表示被所有直径经过的边的数量。

输入输出样例

输入

6
3  1 1000
1  4 10
4  2 100
4  5 50
4  6 100

输出

1110 
2 

分析:

首先,相信如果大家看懂我之前的题目的话求树的直径应该没什么问题,这道题难就难在怎么求所有直径经过的边的数量。在这里我给出解题思路:

首先我们先求出一条直径,答案所包含的边是所有直径的公共边,那么一定也会在我们一开始求的那条直径上出现,我们记录这条直径上的所有点,在对这条直径上的每一个点都dfs,求出来以这个点为起始点并且不包括直径上其他点的最长链长度,用d数组存储。l[i]表示这个点离直径左端点的距离,r[i]表示离直径右端点的距离。(左右其实是随意定的,并没有什么本质差别)

接下来我们从左端点开始遍历直径,若走到直径上的j点时发现 d[j]=r[j],也就是说我们找到了一个以这个点为起始点的链,这条链的长度与该起始点距离原直径右端点的距离相同,换句话说就是我们找到了一条新的直径可以不经过j点右边的点,那么我们就可以标记为rr后跳出循环。

同理我们可以以rr为起始点向左遍历直径,若走到直径上的j点时发现 d[j]=l[j],也就是说我们找到了一个以这个点为起始点的链,这条链的长度与该起始点距离原直径右左端点的距离相同,换句话说就是我们找到了一条新的直径可以不经过j点左边的点,那么我们就可以标记为ll后跳出循环。

这样我们就找了直径必经边的起始点ll和终止点rr,思路大概就是这样,但是还有一个需要注意的就是我们以直径上的每个点为起始点进行dfs找最大链时,不能每次都初始化d[]数组,那样会TLE,我们只能在dfs过程中顺便记录最大值,并于相应的r[j]或l[j]比较,下面我先给出第一个超时的代码,接着给出优化后的代码,希望读者能够注意到这个细微差别!

超时代码:

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
const int N=1e6+10;
int h[N],e[N],ne[N],idx,w[N];
typedef long long ll;
ll d1[N],d2[N];
bool vis[N];//记录直径上的点 
struct point{
	int x;//位置 
	long long l,r;//距离直径左右端点的边权值
	int ansl;//距离直径左端点的边数差
}p[N];
bool cmp(point a,point b)
{
	return a.l<b.l;
}
void add(int x,int y,int z)
{
	e[idx]=y;
	w[idx]=z;
	ne[idx]=h[x];
	h[x]=idx++;
}
void dfs(int x,int father)
{
	for(int i=h[x];i!=-1;i=ne[i])
	{
		int j=e[i];
		if(j==father||vis[j]) continue;
		d1[j]=d1[x]+w[i];
		dfs(j,x);
	}
}
int main()
{
	int n;
	int a,b,c;
	cin>>n;
	memset(h,-1,sizeof h);
	for(int i=1;i<n;i++)
	{
		scanf("%d%d%d",&a,&b,&c);
		add(a,b,c);add(b,a,c);
	}
	ll ans=0,t=0;
	int e1,e2;
	dfs(1,0);
	for(int i=1;i<=n;i++)
		if(d1[i]>t)
		{
			t=d1[i];
			e1=i;
		}
	d1[e1]=0;
	dfs(e1,0);
	for(int i=1;i<=n;i++)
		if(d1[i]>ans)
		{
			ans=d1[i];
			e2=i;
		}
	printf("%lld\n",ans);
	memcpy(d2,d1,sizeof d2);
	d1[e2]=0;
	dfs(e2,0);
	int cnt=0;
	for(int i=1;i<=n;i++)//找直径上的点 
		if(d1[i]+d2[i]==ans)
			p[++cnt]={i,d1[i],d2[i]},vis[i]=true;
	sort(p+1,p+cnt+1,cmp);
	for(int i=1;i<=cnt;i++)
		p[i].ansl=i-1;
	int ll=0,rr=0;
	for(int i=2;i<=cnt;i++)
	{
		long long tmax=0;
		memset(d1,0,sizeof d1);
		dfs(p[i].x,0);
		for(int j=1;j<=n;j++)
			tmax=max(tmax,d1[j]);
		if(p[i].r==tmax)
		{
			rr=p[i].ansl;
			break;
		}
	}
	for(int i=rr+1;i>=1;i--)
	{
		long long tmax=0;
		memset(d1,0,sizeof d1);
		dfs(p[i].x,0);
		for(int j=1;j<=n;j++)
			tmax=max(tmax,d1[j]);
		if(p[i].l==tmax)
		{
			ll=p[i].ansl;
			break;
		}
	}
	printf("%d",rr-ll);
	return 0;
}

优化后代码:

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
const int N=1e6+10;
int h[N],e[N],ne[N],idx,w[N];
typedef long long ll;
ll d1[N],d2[N];
bool vis[N];//记录直径上的点 
struct point{
	int x;//位置
	long long l,r;//距离直径左右端点的边权值
	int ansl;//距离直径左端点的边数差
}p[N];
bool cmp(point a,point b)
{
	return a.l<b.l;
}
void add(int x,int y,int z)
{
	e[idx]=y;
	w[idx]=z;
	ne[idx]=h[x];
	h[x]=idx++;
}
long long tmax;
void dfs(int x,int father)
{
	for(int i=h[x];i!=-1;i=ne[i])
	{
		int j=e[i];
		if(j==father||vis[j]) continue;
		d1[j]=d1[x]+w[i];
		tmax=max(tmax,d1[j]);//在dfs过程中顺便找到以直径上的点为起始点的分枝上距离起始点最远的距离 
		dfs(j,x);
	}
}
int main()
{
	int n;
	int a,b,c;
	cin>>n;
	memset(h,-1,sizeof h);
	for(int i=1;i<n;i++)
	{
		scanf("%d%d%d",&a,&b,&c);
		add(a,b,c);add(b,a,c);
	}
	ll ans=0,t=0;
	int e1,e2;//直径的两个端点 
	dfs(1,0);//找出直径的一个端点 
	for(int i=1;i<=n;i++)
		if(d1[i]>t)
		{
			t=d1[i];
			e1=i;
		}
	d1[e1]=0;
	dfs(e1,0);//找出直径的另一个端点 
	for(int i=1;i<=n;i++)
		if(d1[i]>ans)
		{
			ans=d1[i];
			e2=i;
		}
	printf("%lld\n",ans);
	memcpy(d2,d1,sizeof d2);
	d1[e2]=0;
	dfs(e2,0);
	int cnt=0;
	for(int i=1;i<=n;i++)//找直径上的点
		if(d1[i]+d2[i]==ans)
			p[++cnt]={i,d1[i],d2[i]},vis[i]=true;
	sort(p+1,p+cnt+1,cmp);
	for(int i=1;i<=cnt;i++)//按照离直径左端点的距离从左到右排序 
		p[i].ansl=i-1;
	int ll=0,rr=0;//记录答案 
	memset(d1,0,sizeof d1);
	for(int i=2;i<=cnt;i++)
	{
		tmax=0;
//		memset(d1,0,sizeof d1);//一定要在dfs过程中更新分枝的最大值,否则会TLE 
		dfs(p[i].x,0);
		if(p[i].r==tmax)//找到第一个不经过原直径右面的点的直径就跳出 
		{
			rr=p[i].ansl;
			break;
		}
	}
	
	for(int i=rr+1;i>=1;i--)
	{
		tmax=0;
//		memset(d1,0,sizeof d1);//一定要在dfs过程中更新分枝的最大值,否则会TLE
		dfs(p[i].x,0);
		if(p[i].l==tmax)//找到第一个不经过原直径左面的点的直径就跳出
		{
			ll=p[i].ansl;
			break;
		}
	}
	printf("%d",rr-ll);
	return 0;
}

如果大家有更经典的例题,欢迎大家在评论区评论,看到后我会尽量添加进去!

  • 6
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值