一、树的直径
1.定义
树上任意两节点之间最长的简单路径即为树的直径。
简单路径:一条路径上没有重复的边即为简单路径。
显然,一棵树可能会有多条直径,它们的长度相等。通常我们有两种求解树的直径的方式,分别是两次 DFS 或者树形 DP 的方法,都可以在 O ( n ) O(n) O(n) 的时间求出树的直径。下面分别进行介绍。
2.两次 DFS 求树的直径
(1)算法流程
首先从任意节点 r o o t root root 开始进行第一次 DFS,到达距离其最远的节点,记为 s t a r t start start,然后再从 s t a r t start start 开始做第二次 DFS,到达距离 s t a r t start start 最远的节点,记为 s t a r t start start,则 δ ( s t a r t , f i n a ) \delta(start,fina) δ(start,fina) 即为树的直径。
显然,如果第一次 DFS 到达的节点 s t a r t start start 是直径的一端,那么第二次 DFS 到达的节点 f i n a fina fina 一定是直径的一端。我们只需证明在任意情况下, s t a r t start start 必为直径的一端。
定理:在一棵树上,从任意节点 r o o t root root 开始进行一次 DFS,到达的距离其最远的节点 s t a r t start start 必为直径的一端。
(2)证明
使用反证法。记出发节点为 r o o t root root。设真实的直径是 δ ( s t a r t , f i n a ) \delta(start,fina) δ(start,fina),而从 r o o t root root 进行的第一次 D F S DFS DFS 到达的距离其最远的节点 x x x 不为 s t a r t start start 或 f i n a fina fina。共分三种情况:
- 若 r o o t root root 在 δ ( s t a r t , f i n a ) \delta(start,fina) δ(start,fina) 上 :
有 δ ( r o o t , x ) > δ ( r o o t , f i n a ) → δ ( s t a r t , x ) > δ ( s t a r t , f i n a ) \delta(root,x)>\delta(root,fina)\rightarrow\delta(start,x)>\delta(start,fina) δ(root,x)>δ(root,fina)→δ(start,x)>δ(start,fina) ,与 δ ( s t a r t , f i n a ) \delta(start,fina) δ(start,fina) 为树上任意两节点之间最长的简单路径矛盾。
- 若 r o o t root root 不在 δ ( s t a r t , f i n a ) \delta(start,fina) δ(start,fina)上,且与存在重合路径为 δ ( a , b ) \delta(a,b) δ(a,b):
有 δ ( r o o t , x ) > δ ( r o o t , f i n a ) → δ ( a , x ) > δ ( a , f i n a ) → δ ( s t a r t , x ) > δ ( s t a r t , f i n a ) \delta(root,x)>\delta(root,fina)\rightarrow\delta(a,x)>\delta(a,fina)\rightarrow\delta(start,x)>\delta(start,fina) δ(root,x)>δ(root,fina)→δ(a,x)>δ(a,fina)→δ(start,x)>δ(start,fina) ,与 δ ( s t a r t , f i n a ) \delta(start,fina) δ(start,fina) 为树上任意两节点之间最长的简单路径矛盾。
- 若 r o o t root root 不在 δ ( s t a r t , f i n a ) \delta(start,fina) δ(start,fina)上,且不存在重合路径, δ ( r o o t , x ) \delta(root,x) δ(root,x) 上有一点 x x x 与 δ ( s t a r t , f i n a ) \delta(start,fina) δ(start,fina)上有一点 x ′ x' x′ 直接相连:
有 δ ( r o o t , x ) > δ ( r o o t , f i n a ) ⟹ δ ( x ′ , x ) > δ ( x ′ , f i n a ) ⟹ δ ( x , f i n a ) > δ ( x , f i n a ) ⟹ δ ( s t a r t , x ) > δ ( s t a r t , f i n a ) \delta(root,x)>\delta(root,fina)\Longrightarrow \delta(x',x)>\delta(x',fina)\Longrightarrow\delta(x,fina) > \delta(x,fina)\Longrightarrow\delta(start,x)>\delta(start,fina) δ(root,x)>δ(root,fina)⟹δ(x′,x)>δ(x′,fina)⟹δ(x,fina)>δ(x,fina)⟹δ(start,x)>δ(start,fina),与 δ ( s t a r t , f i n a ) \delta(start,fina) δ(start,fina) 为树上任意两节点之间最长的简单路径矛盾。
综上,三种情况下假设均会产生矛盾,故原定理得证。
(3)模板代码
cpp
void dfs(ll u,ll pre)
{
if(d[u]>d[pos])
pos=u;
for(ll i=p[u];i!=-1;i=e[i].next)
{
ll v=e[i].v,w=e[i].w;
if(v!=pre)
{
d[v]=d[u]+w;
dfs(v,u);
}
}
}
int main()
{
memset(p,-1,sizeof(p));
scanf("%lld",&n);
for(ll i=1;i<n;i++)
{
ll u,v,w;
scanf("%lld%lld%lld",&u,&v,&w);
insert(u,v,w);
insert(v,u,w);
}
dfs(1,0);
memset(d,0,sizeof(d));
start=pos;pos=0;
dfs(start,0);
fina=pos;
printf("%lld\n",d[fina]);
return 0;
}
(4)缺陷
从证明不难看出,这个方法无法处理负边权的情况。如果出现负边权,则无法采用两次 D F S DFS DFS 的方法,而只能采用下面所讲到的树形 D P DP DP 的方法。
2.树形 D P DP DP
(1)算法流程
我们记录当 1 1 1 为树的根时,每个节点作为子树的根向下,所能延伸的最长路径长度 d 1 d_1 d1与次长路径(与最长路径无公共边)长度 d 2 d_2 d2 ,那么直径就是对于每一个点,该点 d 1 + d 2 d_1+d_2 d1+d2 能取到的值中的最大值。
树形 D P DP DP 可以在存在负权边的情况下求解出树的直径。
(2)模板代码
cpp
void dfs(ll u,ll pre)
{
for(ll i=p[u];i!=-1;i=e[i].next)
{
ll v=e[i].v,w=e[i].w;
if(v!=pre)
{
dfs(v,u);
ll temp=d1[v]+w;
if(temp>d1[u])
{
d2[u]=d1[u];
d1[u]=temp;
}
else if(temp>d2[u])
d2[u]=temp;
}
}
d=max(d,d1[u]+d2[u]);
}
int main()
{
freopen("test.txt","r",stdin);
memset(p,-1,sizeof(p));
ll u,v,w;
while(~scanf("%lld%lld%lld",&u,&v,&w))
{
insert(u,v,w);
insert(v,u,w);
}
dfs(1,0);
printf("%lld\n",d);
return 0;
}
二、树的重心
1.定义
对于树上的每一个点,计算其所有子树中最大的子树节点数,这个值最小的点就是这棵树的重心。
子树:指无根树的子树,即包括向上的那棵子树,并且不包括整棵树自身。
2.性质
- 树的重心如果不唯一,则至多有两个,且这两个重心相邻。
- 以树的重心为根时,所有子树的大小都不超过整棵树大小的一半。
- 树中所有点到某个点的距离和中,到重心的距离和是最小的;如果有两个重心,那么到它们的距离和一样。
- 把两棵树通过一条边相连得到一棵新的树,那么新的树的重心在连接原来两棵树的重心的路径上。
- 在一棵树上添加或删除一个叶子,那么它的重心最多只移动一条边的距离。
3.算法流程
在 D F S DFS DFS 中计算每个子树的大小,记录向下的子树的最大大小,利用总点数 - 当前子树的大小得到向上的子树的大小,然后就可以依据定义找到重心了。
4.模板代码
cpp
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cmath>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long ll;
const ll maxn=50010;
struct node
{
ll v;
ll next;
}e[2*maxn];
ll n,t=0,p[maxn];
ll minnode=-1,Min=1e18;
void insert(ll u,ll v)
{
e[t].v=v;
e[t].next=p[u];
p[u]=t++;
}
ll dfs(ll u,ll pre)
{
ll sz=0,Max=0;
for(ll i=p[u];i!=-1;i=e[i].next)
{
ll v=e[i].v;
if(v!=pre)
{
ll son=dfs(v,u);
sz+=son;
Max=max(Max,son);
}
}
sz++;
Max=max(Max,n-sz);
if(Max<Min || (Max==Min && u<minnode))
{
Min=Max;
minnode=u;
}
return sz;
}
ll d[maxn],sum=0;
void getans(ll u,ll pre)
{
for(ll i=p[u];i!=-1;i=e[i].next)
{
ll v=e[i].v;
if(v!=pre)
{
d[v]=d[u]+1;
sum+=d[v];
getans(v,u);
}
}
}
int main()
{
memset(p,-1,sizeof(p));
scanf("%lld",&n);
for(ll i=1;i<=n-1;i++)
{
ll a,b;
scanf("%lld%lld",&a,&b);
insert(a,b);
insert(b,a);
}
ll temp=dfs(1,0);
d[minnode]=0;
getans(minnode,0);
printf("%lld %lld",minnode,sum);
return 0;
}