树的直径
给定一棵树,树的每条边都有一个权值,树中两点之间的距离定义为连接两点的路径上的边权之和。树上最远的两个节点之间的距离被称为树的直径,连接这两点的路径被称为树的最长链。后者通常也可称为直径,即直径既是一个数值概念,也可代指一条路径。
树的直径有两种求法,时间复杂度都是O(n)。
贪心求法:
贪心求直径的方法是任意找一个点为根,dfs整棵树找到距离他最远的点p,为根求出距离它最远的点(x,y)即为直径。
DP求法:
DP求直径的方法是对于每个点记录这个点子树中的最长链及与最长链处于不同子树中的次长链,用每个点的最长链+次长链更新直径,然后再将最长链上传到父节点更新父节点的最长链或次长链。这种求法适用于所有求树的直径的情况。
我们这里主要介绍贪心求法,即两次dfs求树的直径。
算法流程如下:
1.从任意一个节点s出发,通过dfs对树进行一次遍历,求出与s距离最远的节点p。
2.从节点p出发,再通过dfs对树进行一次遍历,求出与p距离最远的节点q。
从p到q的一条路径就是树的直径。
证明如下:
假设确定了直径的一个端点,那么另一个端点一定是距离这个端点最远的点,所以第二次找最远点的贪心一定正确。
我们采用反证法,假设第一次从s开始找,找到的点是p,而存在一个点u,使得以u为根找最远点v形成的直径要比以p为根找最远点形成的直径长。
假设两点间距离用dis表示
如果(p,u)的路径与(u,v)的路径不相交,dis(p,u)+dis(u,v)一定比dis(u,v)长,假设不成立。
如果(p,u)的路径与(u,v)的路径相交,假设两路径的另一交点为x,那么dis(p,x)>>dis(u,x),因为以s为根时p的深度比u的深度深,所以手画一下就能看出来。
树的直径的一些性质:
1、直径两端点一定是两个叶子节点。
2、距离任意点最远的点一定是直径的一个端点,这个基于贪心求直径方法的正确性可以得出。
3、对于两棵树,如果第一棵树直径两端点为(u,v),第二棵树直径两端点为(x,y),用一条边将两棵树连接,那么新树的直径一定是u,v,x,y,中的两个点。
证明如下:
如果新树直径不是原来两棵树中一棵的直径,那么新直径一定经过两棵树的连接边,新直径在原来每棵树中的部分一定是距离连接点最远的点,即一定是原树直径的一个端点。
例题:洛谷 P2195 https://www.luogu.com.cn/problem/P2195
可将题意简化为该模型:
给出一个n个点,m 条边组成的森林,有q组询问:
1.给出点x,求点x所在的树的直径。
2.给出点x,y,要求将x,y 所在的树之间连一条边并构成一棵新的树,满足这个新的树的直径最小。
第一个问题很好解决,只要跑两次dfs求树的直径即可。那么第二个问题呢?
显然我们无法每次真的合并两棵树再重新跑一遍直径,那我们看一下树的合并有什么特殊的性质。
通过几个简单的样例我们可以发现,直径为x的树与直径为y的树合并后形成的新树直径为max{x,y,⌈x/2⌉+⌈y/2⌉+1}。
因为要保证新树直径最小,那我们肯定会连接两棵树的直径的中点。
若两棵树的直径x>y,那我们连接后可以发现,新树的直径大小仍然是x。x<y时同理为y。
若两棵树的直径x=y,那我们连接后可以发现,新树的直径大小为⌈x/2⌉+⌈y/2⌉+1。
因为在长度不同时前者大于后者,长度相同时后者大于前者。所以三者直接取最大值即可。
我们先求出初始的森林中每棵树的直径,合并与判断操作靠并查集实现,然后更新所在树的直径即可。
AC代码如下:
1 #include<bits/stdc++.h> 2 #define ll long long 3 using namespace std; 4 const int N=3e5+5; 5 vector<int> g[N]; 6 int f[N],d[N],ans,p,c[N],n; 7 void dfs(int u,int fa) 8 { 9 if(d[u]>=ans) 10 { 11 ans=d[u];//求的过程中同时更新答案 12 p=u; 13 } 14 for(int i=0;i<g[u].size();i++) 15 { 16 int v=g[u][i]; 17 if(v==fa) continue; 18 d[v]=d[u]+1; 19 dfs(v,u); 20 } 21 } 22 int get_zhi(int x) 23 { 24 ans=0; 25 d[x]=0; 26 dfs(x,0);//以x为根,求出离x最远的节点p 27 ans=0; 28 d[p]=0; 29 dfs(p,0);以p为根,求出离p最远的节点q 30 return ans; 31 } 32 int fa(int x) 33 { 34 if(f[x]==x) return x; 35 return f[x]=fa(f[x]); 36 }//并查集 37 int main() 38 { 39 int m,q; 40 scanf("%d%d%d",&n,&m,&q); 41 for(int i=1;i<=n;i++) f[i]=i;//初始化 42 while(m--) 43 { 44 int x,y; 45 scanf("%d%d",&x,&y); 46 f[fa(x)]=fa(y); 47 g[x].push_back(y); 48 g[y].push_back(x);//读入数据,建图 49 } 50 for(int i=1;i<=n;i++) 51 if(f[i]==i) c[i]=get_zhi(i);//求出每棵树的直径 52 while(q--) 53 { 54 int op,x,y; 55 scanf("%d",&op); 56 if(op==1) 57 { 58 scanf("%d",&x); 59 cout<<c[fa(x)]<<endl;//注意输出答案时要输出x所在的树的直径,即x的祖先所在树的直径 60 } 61 else 62 { 63 scanf("%d%d",&x,&y); 64 x=fa(x); 65 y=fa(y); 66 if(x==y) continue; 67 f[x]=y; 68 c[y]=max(max(c[x],c[y]),(c[x]+1)/2+(c[y]+1)/2+1); 69 } 70 } 71 return 0; 72 }标签:新树,两棵树,直径,节点,最远,dis From: https://www.cnblogs.com/Snoww/p/16648285.html