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级父亲是与该节点深度差为2的祖先。这样定义之后,我们往上跳可以从每次跳1级变成每次跳2级,如果把每次跳的步数由1拓展到2,就可以加速这个过程,具体操作如下:

先将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 }
View Code

 


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 }
View Code

 

第二次则需要求出重链,此次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 }
View Code

 

在求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 }
View Code

 

完整代码:

技术分享
 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 }
View Code

 

以上过程预处理复杂度为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 }
View Code

 


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 }
View Code

 

以上过程用ST表做RMQ问题预处理复杂度为O(nlogn),单次查询复杂度为O(1),总复杂度为O(q+nlogn)。

用线段树处理RMQ问题预处理复杂度为O(n),单次查询复杂度为O(logn),总复杂度为O(qlogn+n)。

例题下次空了再贴吧……

以上是关于LCA的主要内容,如果未能解决你的问题,请参考以下文章

代码源 Div1 - 105#451. Dis(倍增求LCA)

浅谈LCA

代码源 Div1 - 105#451. Dis(倍增求LCA)

LCA

二叉树---最近公共父节点(LCA)

LCA 最近公共祖先