一. 定义
树上任意两点间最大距离(最长路)
二. 性质(重要)
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;
}