LCA
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了LCA相关的知识,希望对你有一定的参考价值。
作为一个数据结构学傻,终于要开始入这个坑了......
之前一直不写有关数据结构的东西,是因为不会画那种很高端的图,所以一直不写,现在看来似乎没有办法避免了……
LCA,全称Least Common Ancestor,即最近公共祖先,顾名思义,LCA(u,v)就是u和v的所有共同祖先中深度最小的,很多算法对于树上一根链的处理通常都需要围绕LCA展开,因此快速求得树上两个节点的LCA是树上算法的基础知识。
先给出一个求LCA的朴素算法,事实上部分求LCA的真正算法就是以其为基础改进而来:
预先遍历整棵树,求得除根节点之外的所有节点的父亲,在询问LCA(u,v)时,先将深度较大的点(不妨设为u)每次向上跳(形象比喻),直至与v处于同一深度,此时判断u,v是否重合,如果重合u(或者v)就是答案,否则将u和v一起向上跳,直至u和v重合。
朴素算法就不贴代码了......
分析算法后便会发现,这个算法的时间复杂度取决于树高,如果树的形态退化成链的话,复杂度将退化为O(n),无法接受。
注意:对LCA原理的讲解中,涉及的所有代码均来自Luogu P3379 【模板】最近公共祖先(LCA) 题目链接
1.倍增LCA
倍增LCA最接近朴素算法。它引入了一个“k级父亲”的概念,下面是递归定义:
(1)0级父亲是一个节点的直接父亲。
(2)k级父亲是一个节点的k-1级父亲的k-1级父亲。
换句话说,一个节点的k级父亲是与该节点深度差为2k 的祖先。这样定义之后,我们往上跳可以从每次跳1级变成每次跳2k 级,如果把每次跳的步数由1拓展到2k ,就可以加速这个过程,具体操作如下:
先将u确定为深度较大的点,然后k从大到小逐个尝试上跳到u的k级父亲,k的上界为log(dep[u])/log(2),下界显然为0,如果上跳之后u的深度小于等于v的深度,就执行上跳操作,然后尝试距离更小的上跳操作。因为这种跳法相当于将u与v的深度差转换为了二进制数,因此能够保证在循环结束时u的深度一定与v相同。这样就完成了第一步操作。
接下来,将u和v一起上跳,也是k从大到小逐个尝试上跳到u,v分别的k级父亲,如果两者的k级父亲存在且不相同,就执行上跳操作,跟第一步操作原理相同,这样做能保证在结束时u和v都是LCA(u,v)的直接儿子,这样u(或者v)的0级父亲就是LCA(u,v)了。
这样做预处理复杂度为O(n),单次查询的复杂度是O(logn)的,显然它是一个在线算法,如果q次查询的话总复杂度为O(qlogn+n)。
代码:
1 #include<bits/stdc++.h> 2 using namespace std; 3 const int maxn=5e5+10,maxm=1e6+10,maxj=20; 4 int heade[maxn],ev[maxm],nexte[maxm]; 5 int fa[maxn][maxj],dep[maxn]; 6 int n,m,tot=0,root; 7 void add_edge(int u,int v){ev[++tot]=v;nexte[tot]=heade[u];heade[u]=tot;} 8 void dfs(int ui)//确定0级父亲和各节点深度 9 { 10 int i,vi; 11 for(i=heade[ui];~i;i=nexte[i]) 12 { 13 vi=ev[i];if(vi==fa[ui][0]){continue;} 14 fa[vi][0]=ui;dep[vi]=dep[ui]+1; 15 dfs(vi); 16 } 17 } 18 void init()//倍增预处理 19 { 20 int i,j; 21 for(j=1;(1<<j)<=n;j++){for(i=1;i<=n;i++){if(fa[i][j-1]!=-1){fa[i][j]=fa[fa[i][j-1]][j-1];}}} 22 } 23 int lca(int u,int v) 24 { 25 int i,j,t; 26 if(dep[u]<dep[v]){swap(u,v);} 27 t=(int)(log(dep[u])/log(2)); 28 for(j=t;j>=0;j--){if(dep[u]-(1<<j)>=dep[v]){u=fa[u][j];}}//先跳至同一深度 29 if(u==v){return u;} 30 for(j=t;j>=0;j--){if(fa[u][j]!=-1&&fa[u][j]!=fa[v][j]){u=fa[u][j];v=fa[v][j];}}//再一起向上跳 31 return fa[u][0]; 32 } 33 int main() 34 { 35 int i,j,u,v; 36 cin>>n>>m>>root; 37 memset(heade,-1,sizeof(heade)); 38 memset(fa,-1,sizeof(fa)); 39 memset(nexte,-1,sizeof(nexte)); 40 for(i=1;i<n;i++){scanf("%d%d",&u,&v);add_edge(u,v);add_edge(v,u);} 41 dfs(root);init(); 42 for(i=1;i<=m;i++) 43 { 44 scanf("%d%d",&u,&v); 45 printf("%d\n",lca(u,v)); 46 } 47 return 0; 48 }
2.树剖LCA
树剖LCA跟朴素算法在细节上有一些差别,但总体上还是加速向上跳的过程,只不过加速的方法变成了剖分。(感觉要把树剖事先在这里写掉了……)
树链剖分,顾名思义,就是将一棵树剖分成若干条链,链上的点互为祖先和子孙的关系,这样上跳从点到点可以变为从一条链上跳到另一条链上,从而实现加速。容易发现,这样加速的效率取决于怎样剖分,链的条数应尽可能少,下面给出一种剖分方法,又称轻重树链剖分。
介绍轻重树链剖分前,先引入“轻重儿子”的概念。所谓u的重儿子,是指在u的所有儿子中子树大小(v的子树大小指以v为根节点的子树的总节点数)最大的一个儿子(如果有多个儿子达到最大值则在其中随机选取一个),u的轻儿子则是除u的重儿子外的其他所有的儿子。然后我们把所有相邻的重儿子连起来,形成了若干条链,我们称之为重链。如果稍稍扩展一下重链的定义,我们可以将单独的一个轻儿子看成一条单独的链,这样就完成了对这棵树的剖分。
剖分的具体操作是对一个树进行两次DFS,第一次需要求出每个节点的深度,父亲,子树大小和重儿子。子树大小可以在回溯时累加得到,重儿子同样可以在回溯时进行判断。
代码:
1 void fdfs(int ui)//第一次DFS 2 { 3 int i,vi; 4 size[ui]=1;son[ui]=0;//初始化size和son 5 for(i=heade[ui];i!=-1;i=nexte[i]) 6 { 7 vi=ev[i]; 8 if(fa[ui]==vi){continue;} 9 dep[vi]=dep[ui]+1;fa[vi]=ui; 10 fdfs(vi);size[ui]+=size[vi];//回溯统计size 11 if(size[son[ui]]<size[vi]){son[ui]=vi;}//维护son 12 } 13 }
第二次则需要求出重链,此次DFS函数的状态中需要记录访问的节点和所在链的顶端节点,并在函数中通过一个数组来保存所有节点所在链的顶端节点,从而实现链与链之间的上跳。根据轻重剖分的定义,如果节点u存在重儿子son(u),则对son(u)进行DFS,由于u和son(u)在一条重链上,因此top(u)=top(son(u))。然后对u的轻儿子v进行DFS,此时top(v)=v。这样就完成了树链剖分。我们可以证明这样进行剖分最多只会剖出logn条重链,从而能保证剖分的效率。
代码:
1 void sdfs(int ui,int pre)//第二次DFS 2 { 3 int i,vi; 4 top[ui]=pre;//该节点所在重链的顶端节点 5 if(son[ui]){sdfs(son[ui],pre);}//继续向下拉重链 6 for(i=heade[ui];i!=-1;i=nexte[i]) 7 { 8 vi=ev[i]; 9 if(vi==fa[ui]||vi==son[ui]){continue;} 10 sdfs(vi,vi);//以轻儿子为顶端节点向下拉重链 11 } 12 }
在求LCA(u,v)时,是每次从一条链跳到上面的一条链,为了确保不会跳过LCA,需要设置一个合理的方法。若u和v在同一条重链中,则LCA(u,v)显然为u和v中深度较小的点。若u和v不在同一重链中,则先将u确定为深度较大的点,然后将u赋为u所在重链顶端的节点的父亲,然后继续判断u和v是否在同一重链中,直至两点在同一重链中。
代码:
1 int lca(int u,int v) 2 { 3 int x=top[u],y=top[v]; 4 while(x!=y) 5 { 6 if(dep[x]<dep[y]){swap(x,y);swap(u,v);} 7 u=fa[x];x=top[u];//链与链之间的转换 8 } 9 return dep[u]<dep[v]?u:v; 10 }
完整代码:
1 #include<bits/stdc++.h> 2 using namespace std; 3 const int maxn=5e5+10,maxm=1e6+10; 4 int heade[maxn],ev[maxm],nexte[maxm]; 5 int size[maxn],son[maxn],dep[maxn],fa[maxn]; 6 int top[maxn]; 7 int n,m,s,tot=0; 8 void add_edge(int u,int v){ev[++tot]=v;nexte[tot]=heade[u];heade[u]=tot;} 9 void fdfs(int ui)//第一次DFS 10 { 11 int i,vi; 12 size[ui]=1;son[ui]=0;//初始化size和son 13 for(i=heade[ui];i!=-1;i=nexte[i]) 14 { 15 vi=ev[i]; 16 if(fa[ui]==vi){continue;} 17 dep[vi]=dep[ui]+1;fa[vi]=ui; 18 fdfs(vi);size[ui]+=size[vi];//回溯统计size 19 if(size[son[ui]]<size[vi]){son[ui]=vi;}//维护son 20 } 21 } 22 void sdfs(int ui,int pre)//第二次DFS 23 { 24 int i,vi; 25 top[ui]=pre;//该节点所在重链的顶端节点 26 if(son[ui]){sdfs(son[ui],pre);}//继续向下拉重链 27 for(i=heade[ui];i!=-1;i=nexte[i]) 28 { 29 vi=ev[i]; 30 if(vi==fa[ui]||vi==son[ui]){continue;} 31 sdfs(vi,vi);//以轻儿子为顶端节点向下拉重链 32 } 33 } 34 int lca(int u,int v) 35 { 36 int x=top[u],y=top[v]; 37 while(x!=y) 38 { 39 if(dep[x]<dep[y]){swap(x,y);swap(u,v);} 40 u=fa[x];x=top[u];//链与链之间的转换 41 } 42 return dep[u]<dep[v]?u:v; 43 } 44 int main() 45 { 46 int i,j,u,v; 47 memset(heade,-1,sizeof(heade)); 48 cin>>n>>m>>s; 49 for(i=1;i<n;i++){scanf("%d%d",&u,&v);add_edge(u,v);add_edge(v,u);} 50 fdfs(s);sdfs(s,s); 51 for(i=1;i<=m;i++){scanf("%d%d",&u,&v);printf("%d\n",lca(u,v));} 52 return 0; 53 }
以上过程预处理复杂度为O(n),单次查询复杂度同样为O(logn),为在线算法,q次查询复杂度为O(qlogn+n)。
3.Tarjan算法
以Tarjan命名的算法有很多,如求强连通分量、双连通分量的算法等,这里介绍的是通过DFS和并查集求LCA的离线算法。
在线算法和离线算法的区别,就是在线算法可以即时处理每一个询问,询问之间互相独立,不需要依赖询问与询问之间的关系。通俗地说就是“来一个打一个”,而离线算法则会利用询问之间的关系进行处理,所以它需要事先知道全部询问,最后进行统一处理。通俗地说就是“等人来齐了再放大招”。
由于本人蒟蒻,Tarjan算法我至今无法清楚地讲出算法的原理,只能复述一遍算法过程(全靠背代码……):
在访问到节点u时,新增一个元素只有u的集合,并将u设为已访问,然后遍历u的未访问过的邻接点vi,对其进行访问,在回溯时将vj所在集合与u所在集合进行合并。访问完所有邻接点后,遍历所有询问中与u有关的询问,判断询问中另一个点vj是否已被访问,若已被访问,则vj所在集合的代表元就是两者的LCA(并不知道为什么……)。
由于只要对整棵树和所有询问遍历一遍就能解决问题,Tarjan算法在解决n个节点,q个询问的LCA问题时复杂度为O(n+q),相比在线算法实现了复杂度级别的优化。
代码:
1 #include<bits/stdc++.h> 2 using namespace std; 3 const int maxn=5e5+10,maxm=1e6+10; 4 int heade[maxn],ev[maxm],nexte[maxm];//原图 5 int head[maxn],ed[maxm],ep[maxm],nextt[maxm];//询问图 6 int father[maxn],rec[maxn],isvis[maxn];//rec用来记录LCA输出顺序 7 int n,m,s,tot=0,num=0; 8 void add_edge(int u,int v){ev[++tot]=v;nexte[tot]=heade[u];heade[u]=tot;}//原图添边 9 void add(int u,int v,int p){ed[++num]=v;ep[num]=p;nextt[num]=head[u];head[u]=num;}//询问图添边 10 int find(int x){if(father[x]==x){return x;}return father[x]=find(father[x]);} 11 void dfs(int ui) 12 { 13 int i,vi,pi; 14 father[ui]=ui;isvis[ui]=1;//新增集合 15 for(i=heade[ui];i!=-1;i=nexte[i])//遍历原图中邻接点 16 { 17 vi=ev[i];if(isvis[vi]){continue;} 18 dfs(vi);father[vi]=find(ui); 19 } 20 for(i=head[ui];i!=-1;i=nextt[i]){vi=ed[i];pi=ep[i];if(!isvis[vi]){continue;}rec[pi]=find(vi);}//遍历询问图中邻接点 21 } 22 int main() 23 { 24 int i,j,u,v; 25 cin>>n>>m>>s;memset(heade,-1,sizeof(heade));memset(head,-1,sizeof(head)); 26 for(i=1;i<n;i++){scanf("%d%d",&u,&v);add_edge(u,v);add_edge(v,u);} 27 for(i=1;i<=m;i++){scanf("%d%d",&u,&v);add(u,v,i);add(v,u,i);}//以链表的形式存下所有与u相关的询问(形式上相当于另一张图) 28 dfs(s); 29 for(i=1;i<=m;i++){printf("%d\n",rec[i]);} 30 return 0; 31 }
4.欧拉序+RMQ
其实不是很想写这个在线算法……因为内存占用大思维又复杂,一般会这个算法的都会倍增的吧(然而教练NOIP2016前就给我讲了这个算法……)
欧拉序是基于DFS时间戳的,具体操作是当开始访问节点u时,时间戳++,对u的邻接点v访问结束回溯时时间戳再次++,将时间戳为i时访问到的节点设为ai,就形成了一个序列,称为欧拉序。
记欧拉序中第i个节点在树中的编号为ver[i],欧拉序中第i个节点的深度为dep[i],树中编号为i的节点在欧拉序中第一次出现的位置为first[i],则可以证明(其实是我自己不会证明……)LCA(u,v)为first[u]到first[v]之间dep最小的点,这样LCA问题就转化为RMQ问题,RMQ问题可以用ST表或者线段树解决。
代码:
1 #include<bits/stdc++.h> 2 using namespace std; 3 const int maxn=500000+10; 4 int heade[maxn],ev[2*maxn],nexte[2*maxn],isvis[maxn]; 5 int ver[2*maxn],a[2*maxn],first[maxn],rmq[2*maxn][21],pos[2*maxn][21]; 6 int n,m,s,tot=0,num=1; 7 void add(int u,int v) 8 { 9 tot++;ev[tot]=v; 10 nexte[tot]=heade[u]; 11 heade[u]=tot; 12 } 13 void dfs(int u,int cur) 14 { 15 int i,j,ui; 16 ver[num]=u;a[num]=cur;isvis[u]=1; 17 if(!first[u]){first[u]=num;} 18 num++; 19 for(i=heade[u];i!=-1;i=nexte[i]) 20 { 21 ui=ev[i]; 22 if(!isvis[ui]) 23 { 24 dfs(ui,cur+1); 25 ver[num]=u;a[num]=cur; 26 num++; 27 } 28 } 29 } 30 void rmq_list() 31 { 32 int i,j,m; 33 m=(int)(log(num)/log(2)); 34 for(i=1;i<=num;i++){rmq[i][0]=a[i];pos[i][0]=i;} 35 for(j=1;j<=m;j++) 36 { 37 for(i=1;i+(1<<j)-1<=num;i++) 38 { 39 if(rmq[i][j-1]<rmq[i+(1<<(j-1))][j-1]) 40 { 41 rmq[i][j]=rmq[i][j-1]; 42 pos[i][j]=pos[i][j-1]; 43 } 44 else 45 { 46 rmq[i][j]=rmq[i+(1<<(j-1))][j-1]; 47 pos[i][j]=pos[i+(1<<(j-1))][j-1]; 48 } 49 } 50 } 51 } 52 void rmq_query(int x,int y) 53 { 54 int i,j,m,p; 55 m=(int)(log(y-x+1)/log(2)); 56 if(rmq[x][m]<rmq[y-(1<<m)+1][m]) 57 { 58 p=pos[x][m]; 59 printf("%d\n",ver[p]); 60 } 61 else 62 { 63 p=pos[y-(1<<m)+1][m]; 64 printf("%d\n",ver[p]); 65 } 66 } 67 void lca(int u,int v) 68 { 69 int x,y; 70 x=first[u];y=first[v]; 71 if(x>y){swap(x,y);} 72 rmq_query(x,y); 73 } 74 int main() 75 { 76 int i,j,u,v; 77 cin>>n>>m>>s; 78 memset(heade,-1,sizeof(heade)); 79 memset(nexte,-1,sizeof(nexte)); 80 memset(first,0,sizeof(first)); 81 memset(a,127,sizeof(a)); 82 for(i=1;i<n;i++) 83 { 84 scanf("%d%d",&u,&v); 85 add(u,v);add(v,u); 86 } 87 dfs(s,1);num--; 88 rmq_list(); 89 //cout<<"first:";for(i=1;i<=n;i++){cout<<first[i]<<" ";}cout<<endl; 90 //cout<<"ver:";for(i=1;i<=num;i++){cout<<ver[i]<<" ";}cout<<endl; 91 //cout<<"a:";for(i=1;i<=num;i++){cout<<a[i]<<" ";}cout<<endl; 92 for(i=1;i<=m;i++) 93 { 94 scanf("%d%d",&u,&v); 95 lca(u,v); 96 } 97 return 0; 98 }
以上过程用ST表做RMQ问题预处理复杂度为O(nlogn),单次查询复杂度为O(1),总复杂度为O(q+nlogn)。
用线段树处理RMQ问题预处理复杂度为O(n),单次查询复杂度为O(logn),总复杂度为O(qlogn+n)。
例题下次空了再贴吧……
以上是关于LCA的主要内容,如果未能解决你的问题,请参考以下文章
代码源 Div1 - 105#451. Dis(倍增求LCA)