最近公共祖先(LCA)模板
以下转自:https://www.cnblogs.com/JVxie/p/4854719.html
首先是最近公共祖先的概念(什么是最近公共祖先?):
在一棵没有环的树上,每个节点肯定有其父亲节点和祖先节点,而最近公共祖先,就是两个节点在这棵树上深度最大的公共的祖先节点。
换句话说,就是两个点在这棵树上距离最近的公共祖先节点。
所以LCA主要是用来处理当两个点仅有唯一一条确定的最短路径时的路径。
有人可能会问:那他本身或者其父亲节点是否可以作为祖先节点呢?
答案是肯定的,很简单,按照人的亲戚观念来说,你的父亲也是你的祖先,而LCA还可以将自己视为祖先节点。
举个例子吧,如下图所示4和5的最近公共祖先是2,5和3的最近公共祖先是1,2和1的最近公共祖先是1。
这就是最近公共祖先的基本概念了,那么我们该如何去求这个最近公共祖先呢?
通常初学者都会想到最简单粗暴的一个办法:对于每个询问,遍历所有的点,时间复杂度为O(n*q),很明显,n和q一般不会很小。
常用的求LCA的算法有:Tarjan(离线)、倍增法(在线)
下面贴两种写法的代码,以HDU 2586 How far away ?为例:
题目大意:
给你一棵树n个节点,m次询问,每次询问树上节点u和v的最小距离。
Tarjan(时间复杂度O(n+Qlogn))
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 #include<algorithm> 5 #include<vector> 6 using namespace std; 7 const int N=4e4+5; 8 9 struct node{ 10 int to,w; 11 node(int to,int w):to(to),w(w){} 12 }; 13 14 struct qnode{ 15 int to,id; 16 qnode(int to,int id):to(to),id(id){} 17 }; 18 19 vector<node>v[N]; 20 vector<qnode>q[N]; 21 bool vis[N]; //vis[i]表示点i是否被访问过 22 int res[N],root[N],dis[N]; //res记录答案 23 24 int find(int x){ 25 return root[x]==x?x:root[x]=find(root[x]); 26 } 27 28 void lca(int u){ 29 vis[u]=true; 30 for(int i=0;i<v[u].size();i++){ 31 node t=v[u][i]; 32 if(!vis[t.to]){ 33 dis[t.to]=dis[u]+t.w; 34 lca(t.to); 35 root[t.to]=u; 36 } 37 } 38 for(int i=0;i<q[u].size();i++){ 39 qnode t=q[u][i]; 40 if(vis[t.to]){ 41 res[t.id]=dis[u]+dis[t.to]-2*dis[find(t.to)]; 42 } 43 } 44 } 45 46 int main(){ 47 int t; 48 scanf("%d",&t); 49 while(t--){ 50 int n,m; 51 scanf("%d%d",&n,&m); 52 memset(vis,false,sizeof(vis)); 53 memset(dis,0,sizeof(dis)); 54 for(int i=1;i<=n;i++){ 55 root[i]=i; 56 v[i].clear(); 57 } 58 for(int i=1;i<=m;i++){ 59 q[i].clear(); 60 } 61 for(int i=1;i<=n-1;i++){ 62 int a,b,c; 63 scanf("%d%d%d",&a,&b,&c); 64 v[a].push_back(node(b,c)); 65 v[b].push_back(node(a,c)); 66 } 67 for(int i=1;i<=m;i++){ 68 int a,b; 69 scanf("%d%d",&a,&b); 70 q[a].push_back(qnode(b,i)); 71 q[b].push_back(qnode(a,i)); 72 } 73 lca(1);
77 for(int i=1;i<=m;i++){ 78 printf("%d\n",res[i]); 79 } 80 } 81 return 0; 82 }
倍增法(时间复杂度O(nlogn+Qlogn))
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 #include<vector> 5 #include<algorithm> 6 using namespace std; 7 const int N=1e5+5; 8 9 struct node{ 10 int to,w; 11 node(int to,int w):to(to),w(w){} 12 }; 13 vector<node>v[N]; 14 15 int n,m; 16 int depth[N],fa[N][31],dis[N]; 17 bool vis[N]; 18 19 20 void init(){ 21 memset(depth,0,sizeof(depth)); 22 memset(fa,0,sizeof(fa)); 23 memset(dis,0,sizeof(dis)); 24 memset(vis,false,sizeof(vis)); 25 for(int i=1;i<=n;i++) v[i].clear(); 26 } 27 28 void dfs(int u){ 29 vis[u]=true; 30 for(int i=0;i<v[u].size();i++){ 31 node t=v[u][i]; 32 if(!vis[t.to]){ 33 depth[t.to]=depth[u]+1; 34 fa[t.to][0]=u; 35 dis[t.to]=dis[u]+t.w; 36 dfs(t.to); 37 } 38 } 39 } 40 41 //倍增,处理fa数组 42 void bz(){ 43 for(int j=1;j<=30;j++){ 44 for(int i=1;i<=n;i++){ 45 fa[i][j]=fa[fa[i][j-1]][j-1]; 46 } 47 } 48 } 49 50 int lca(int x,int y){ 51 //保证深度大的点为x 52 if(depth[x]<depth[y]) 53 swap(x,y); 54 int dc=depth[x]-depth[y]; 55 for(int i=0;i<30;i++){ 56 if(1<<i&dc) //一个判断,模拟下就会清楚 57 x=fa[x][i]; 58 } 59 if(x==y) return x; //如果深度一样,两个点相同,直接返回 60 for(int i=29;i>=0;i--){ 61 if(fa[x][i]!=fa[y][i]){ //跳2^i不一样,就跳,否则不跳 62 x=fa[x][i]; 63 y=fa[y][i]; 64 } 65 } 66 x=fa[x][0]; 67 return x; 68 } 69 70 int main(){ 71 int t; 72 scanf("%d",&t); 73 while(t--){ 74 init(); 75 scanf("%d%d",&n,&m); 76 for(int i=1;i<=n-1;i++){ 77 int a,b,c; 78 scanf("%d%d%d",&a,&b,&c); 79 v[a].push_back(node(b,c)); 80 v[b].push_back(node(a,c)); 81 } 82 dfs(1); 83 bz(); 84 for(int i=1;i<=m;i++){ 85 int x,y; 86 scanf("%d%d",&x,&y); 87 printf("%d\n",dis[x]+dis[y]-2*dis[lca(x,y)]); 88 } 89 } 90 return 0; 91 }
相关题目了解一下(转自这里):
hdu 2586 How far away ?
模板题,直接求LCA,可以在线,离线算法均可解,可以测试一下模板
poj 1986 Distance Queries
模板题,直接求LCA
hdu 2874 Connections between cities
模板题,不过不是树是森林,所以某些点不存在LCA,要做判断
zoj 3195 Design the city
任然算是模板题,上面的题要求两点间的最短距离,这里要求3点间的最短距离,其实就是两两之间求一次LCA并计算出距离,然后相加除以2即可
hdu 3078 Network
LCA + 修改点权值 + 排序:每个点有初始的权值,一边查询一边伴随着修改某些点的权值,查询是从a到b路径中第k大的权值是多少。不需要太多的技巧,修改操作就直接修改,查询操作先求LCA,然后从a走到b保存下所有的权值,排序,然后直接输出即可
poj 2763 Housewife Wind
LCA + 修改边权:一边查询两点间的距离,一边修改某些边权。对于修改了某些边的边权,就要从此开始遍历下面的子孙后代更改他们的dir值(点到根的距离)。也不需要太多技巧,直接按题意实现即可,不过时间比较糟糕,用线段树或树状数组可以对修改操作进行优化,时间提升很多
poj 3694 Network
连通分量 + LCA : 先缩点,再求LCA,并且不断更新,这题用了朴素方法来找LCA,并且在路径上做了一些操作
poj 3417 Network
LCA + Tree DP : 在运行Tarjan处理完所有的LCA询问后,进行一次树DP,树DP不难,但是要想到用树DP并和这题结合还是有点难度
poj 3728 The merchant
LCA + 并查集的变形,优化:好题,难题,思维和代码实现都很有难度,需要很好地理解Tarjan算法中并查集的本质然后灵活变形,需要记录很多信息(有点dp的感觉)
hdu 3830 Checkers
LCA + 二分:好题,有一定思维难度。先建立出二叉树模型,然后将要查询的两个点调整到深度一致,然后二分LCA所在的深度,然后检验