树的直径总结

一. 定义

        树上任意两点间最大距离(最长路)

二. 性质(重要) 

        1. 设树T存在直径a1~b1,a2~b2......an~bn,从树上任意点c搜索最远点,可能有多个最远点,它们与c的距离一定相同,且任取一最远点,一定是某条直径的端点,因此最长距离d = max(dis(c, a1), dis(c, b1)) = max(dis(c, a2), dis(c, b2)) = ... = max(dis(c, an), dis(c, bn))。

        推论:以某直径端点a搜索最远点,可能有多个最远点,且a与这些最远点距离都为树上最大距离,即a与它们都能组成直径。  

        证明见:树的直径(最长路) 的详细证明 - Because Of You - 博客园 (cnblogs.com)

        2. 一棵树的所有直径必定以同一点为中点。

        证明见:与图论的邂逅01:树的直径&基环树&单调队列 - 修电缆的建筑工 - 博客园 (cnblogs.com)

        3. 只有a1~b1直径上的点i才满足这个条件:d1[i]+d2[i] == d1[b1],其中d1[i]表示i点距a1的距离,d2[i]表示i点距b1的距离。

        证明:当i点在直径a1~b1上时,显然成立。当i点不在直径上时,反证法,假设满足d1[i]+d2[i] == d1[b1],首先i一定通过某条路径与直径上某点j连通,因为在树上,所以d1[i]与d2[i]路径唯一,有d1[i] = dis(a1, j)+dis(j, i),d2[i] = dis(a2, j)+dis(j, i),二者相加显然不等于dis(a1, j)+dis(j, a2),因此得证。

三. 两种方法求直径长度 

        1. 根据性质及推论,可以通过两次dfs/bfs找到一条直径的两个端点,并得到直径长度。

        具体代码如下:

int _max = 0, id;//id记录最远点标号 
 
void dfs(int now, int fa, int dis)//dis记录起点距离now的距离
{
	if(dis > _max)
	{
		_max = dis;
		id = now;
	}
	for(int i = head[now]; i; i = edges[i].next)//链式前向星遍历子节点 
	{
		if(edges[i].v == fa)//如果下一步要原路返回就跳过,防止无限递归
			continue;
		dfs(edges[i].v, now, dis+edges[i].w);
	}
}

        第一次调用时任选起点,这里以1为例,同时起点的父节点也可以任取:dfs(1,0,0)

        递归结束后id中保存距离起点最远的节点标号。

        第二次调用时以id为起点:dfs(id,0,0)

        递归结束后_max保存树上最长距离,即树的直径。

        2. 树形dp,dp1[i]记录以i为根的子树中最长路径长度,dp2[i]记录以i为根的子树中次长路径长度。(实际的根不变)

        具体代码如下:

void dfs(int now, int fa)//dfs的任务就是更新dp[now]
{
	for(int i = head[now]; i; i = edge[i].next)//遍历每条边 
	{
		if(edge[i].to == fa)
			continue;
		dfs(edge[i].to, now);
		if(dp1[edge[i].to]+edge[i].w > dp1[now])
		{
			dp2[now] = dp1[now];
			dp1[now] = dp1[edge[i].to]+edge[i].w;
		}
		else if(dp2[now] < dp1[edge[i].to]+edge[i].w)
			dp2[now] = dp1[edge[i].to]+edge[i].w;
	}
	ans = max(dp1[now]+dp2[now], ans);
}

        需要注意的是,因为树的直径上不允许有重复边,不可以任取某点为根(实际的根改变),找出其最长路径与次长路径,然后把二者加和作为树的直径。

       比如这棵树:

        转化为以3为根节点后:

 

        直径显然不是3+3=6,要排除重复路径的干扰。

        如果排除重复路径,以3为根的最长路径为3,次长路径为2,加和即为直径5。

        但其实这样也是错误的,如果7、8节点向下连一条很长的链,那直径应该只过4,不过3。

四. 经典例题

        1. Book of Evil CF337D

        转化为子图(还是棵树)上的直径。

        2. Three Paths on a Tree CodeForces CF615F

        先求出直径,再去枚举第三个点。

        3. Computer HDU 2196

        寻找树上三点间距离的最大值。可以发现一定有一组最优解包含直径,之后枚举第三个点,使两端点到第三个点距离和最大即可。选直径时任取一条即可,因为通过画图发现,在不同的直径上向两侧找最远距离是一样的。

        4. Tree Restoring AtCoder - agc005_c

        给定各点能到的最远距离,判断能否构成一棵树。各点最远距离最大值一定为直径,之后枚举直径长度奇偶性并结合图像即可。

        分析及代码如下:

#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
//问题等价于用给定的各点最远距离能否构造出一棵树,根据直径去构造,考虑向直径两侧加点 
//最长的边一定是一条直径 
//分直径长度奇偶讨论
//通过画图可以发现,直径为奇数时,最短的最远距离所在点为两个中间点,值为(len+1)/2
//直径为偶数时,最短的最远距离所在点为唯一的中间点,值为len/2
//同时直径上的点最远距离已经确定,直径为偶数时,len/2的点恰有一个,len/2+1~len的点至少有两个
//直径为奇数时,(len+1)/2的点恰有两个,(len+1)/2+1~len的点至少有两个
//满足上述数量关系后,多余的点总是可以任意加入到直径两侧而不影响之前的选择 
int n, a[105], len;
int num[105];//num[i]记录最远距离为i的点数 

bool check()
{
	if(len&1)//如果直径为奇数 
	{
		if(num[(len+1)/2] != 2)
			return false;
		for(int i = 1; i <= (len+1)/2-1; i++)
			if(num[i])
				return false;
		for(int i = (len+1)/2+1; i <= len; i++)
			if(num[i] < 2)
				return false;		
	}
	else
	{
		if(num[len/2] != 1)
			return false;
		for(int i = 1; i <= len/2-1; i++)
			if(num[i])
				return false;
		for(int i = len/2+1; i <= len; i++)
			if(num[i] < 2)
				return false;
	}
	return true;	
}

signed main()
{
	cin >> n;
	for(int i = 1; i <= n; i++)
	{
		cin >> a[i];
		len = max(len, a[i]); 
		num[a[i]]++;
	}
	if(check())
		puts("Possible");
	else
		puts("Impossible");
		
	return 0;
}

        5. Two POJ1849

        考虑将树沿直径展开,如果起点在直径上,最优解一定是分别向两侧端点走,遇到分支就把分支走完,这样答案为边权和*2-直径长度。如果起点不在直径上,最优解为先向外侧走完分支,之后往直径上走,转为第一种情况,答案也为边权和*2-直径长度。

        代码如下:

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <vector>
#define pii pair<int, int>
using namespace std;
//最优情况是分别沿着直径走,走到分支点就先上去再下来,答案是权值总和*2-直径长度 
vector<pii> a[50000];
int _max, id;

void dfs(int now, int fa, int dis)
{
	if(_max < dis)
	{
		_max = dis;
		id = now; 
	}
	for(int i = 0; i <= a[now].size()-1; i++)
	{
		if(fa == a[now][i].first)
			continue;
		dfs(a[now][i].first, now, dis+a[now][i].second);
	}
}

signed main()
{
	int n, s, ans = 0;
	cin >> n >> s;
	int u, v, w;
	for(int i = 1; i <= n-1; i++)
	{
		scanf("%d%d%d", &u, &v, &w);
		ans += w;
		a[u].push_back(make_pair(v, w));
		a[v].push_back(make_pair(u, w));
	} 
	_max = 0;
	dfs(1, 0, 0);
	_max = 0;
	dfs(id, 0, 0);
	ans *= 2;
	ans -= _max;
	cout << ans;
	return 0;
}

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值