最近公共祖先(LCA)

  树上两点的最近公共祖先

  对于有根树T的两个节点u和v,最近公共祖先LCA(T,u,v)表示一个接点x,满足x是u、v的祖先且x的深度尽可能大。对于点x来说,有一点非常特殊,那就是从u到v的路劲一定经过点x。

  下面讨论用Tarjan算法求解该问题。

  Tarjan算法基于深度优先搜索的框架,对于新搜索到的一个节点,首先创建由这个节点构成的集合,再对当前点的每个子树进行搜索,每搜索完一棵子树,则可以确定子树内的LCA询问都已解决。其他的LCA询问的结果必然在这个子树之外,这时把子树所形成的集合与当前节点的集合合并,并将当前节点设为这个集合的祖先。之后继续搜索下一棵子树,直到当前节点的所有子树搜索完。这时把当前节点也设为已被检查过的,同时可以处理有关当前节点的LCA询问,如果有一个从当前节点到节点v的询问,且v已被检查过,则由于进行的是深度优先搜索,当前节点与v的最近公共祖先一定还没有被检查,而这个最近公共祖先的包含v的子树一定已经搜索过了,那么这个最近公共祖先一定是v所在集合的祖先。

  伪代码:

  对于每一点u:

  ①建立以u为代表元素的集合。

  ②遍历与u相连的节点v,如果没有被访问过,对于v使用Tarjan算法,结束后,将v的集合并入u的集合。

  ③对于与u有关的询问(u,v),如果v被访问过,则结果就是v所在集合的代表元素。

  算法实现:

  使用链式前向星存储图和所有询问,head[]和edge[],qhead[]和qedge[]表示询问。由于链式前向星只能存储有向边,也就是说用其存储图和询问的时候要存储(u,v)和(v,u)两次。即对于每条询问在qedge中有两条,但是只会对其中一条有应答,当然应答时可以对另一条直接赋值。对于集合的操作用并查集来完成。

  代码如下:

 1 int p[maxn];
 2 int head[maxn];
 3 int qhead[maxn];
 4 struct NODE
 5 {
 6     int to,next,lca;
 7 };
 8 NODE edge[maxm];
 9 NODE qedge[maxq];
10 int find(int x)
11 {
12     if(p[x]!=x)p[x]=find(p[x]);
13     return p[x];
14 }
15 bool vis[maxn];
16 void LCA(int u)
17 {
18     p[u]=u;
19     int k;
20     vis[u]=true;
21     for(k=head[u];k!=-1;k=edge[k].next)
22     {
23         if(!vis[edge[k].to])
24         {
25             LCA(edge[k].to);
26             p[edge[k].to]=u;
27         }
28     }
29     for(k=qhead[u];k!=-1;k=qedge[k].next)
30     {
31         if(vis[qedge[k].to])
32         {
33             qedge[k].lca=find(qedge[k].to);
34             qedge[k^1].lca=qedge[k].lca;
35         }
36     }
37 }
View Code

  时间复杂度为O(n+q),非常高效,不过需要记录所有的询问后再应答,是离线的算法。

 

  对于LCA问题,还可以通过O(n)的处理转化成RMQ问题,在O(nlogn)的时间内做预处理后形成在线的算法,可参阅:

  http://wenku.baidu.com/link?url=Q9cFtvsawTALrkkir_Ni-OxvTd8Y29PPg5py3eC-q8SSvMlabwCgPUxvE_E7mkiu4_SUWoOYmTcVNrzLVxSsEhmcM7pi7-mKjyFCb8RdwZe

  下面再介绍一种实现办法:

  算法分析:

  对于每个节点v,记录anc[v][k],表示从他向上走(2^k)步之后到达的节点(如果越过了根节点,那么anc[v][k]就是根节点)。  

  dfs函数对树进行dfs,先求出anc[v][0],再利用anc[v][k]=anc[anc[v][k-1]][k-1]求出其他anc[v][k]的值。

  swim(x,k)函数从节点x向上移动k步,并将x赋为新走到的节点。

  find(x,y)函数寻找x和y的LCA。首先利用swim,将x,y调整到同一高度。如果此时x和y重合,那么这就是我们要找的LCA。如果他们不重合,就不断地寻找一个最小的k,使得anc[x][k]=anc[y][k](这说明向上走(2^k)步越过了x,y的LCA),然后x,y同时向上移动2^(k-1)步,显然新的x,y和原来的x,y有相同的LCA。直到k=0,这说明此时x,y的父节点anc[x][0]和anc[y][0]重合,并且就是我们要找的LCA。

  代码如下:

 1 /*
 2 void dfs(int root);复杂度:O(N)
 3 输入: root
 4        head
 5        point
 6        next
 7 输出: anc
 8 
 9 int find(int x,int y);
10 复杂度: O(log N)
11 输入: x,y
12 输出: 点x,y的LCA
13 */
14 void dfs(int root)
15 {
16     static int Stack[maxn];
17     int top=0;
18     dep[root]=1;
19     for(int i=0;i<maxh;i++)
20     anc[root][i]=root;
21     Stack[++top]=root;
22     memset(head,0,sizeof(head));
23     while(top)
24     {
25         int x=Stack[top];
26         if(x!=root)
27         {
28             for(int i=1;i<maxh;i++)
29             {
30                 int y=anc[x][i-1];
31                 anc[x][i]=anc[y][i-1];
32             }
33         }
34         for(int &i=head[x];i;i=next[i])
35         {
36             int y=point[i];
37             if(y!=anc[x][0])
38             {
39                 dep[y]=dep[x]+1;
40                 anc[y][0]=x;
41                 Stack[++top]=y;
42             }
43         }
44         while(top&&head[Stack[top]]==0)top--;
45     }
46 }
47 void swim(int &x,int H)
48 {
49     for(int i=0;H>0;i++)
50     {
51         if(H&1)x=anc[x][i];
52         H/=2;
53     }
54 }
55 int lca(int x,int y)
56 {
57     int i;
58     if(dep[x]>dep[y])swap(x,y);
59     swim(y,dep[y]-dep[x]);
60     if(x==y)return x;
61     while(true)
62     {
63         for(i=0;anc[x][i]!=anc[y][i];i++);
64         if(i==0)return anc[x][0];
65         x=anc[x][i-1];
66         y=anc[y][i-1];
67     }
68     return -1;
69 }
View Code

  

参考文献:《图论及应用》哈尔滨工业大学出版社

特此申明:严禁转载

2014-02-20

 

 

 

 

posted @ 2014-02-20 01:20  EtheGreat  阅读(565)  评论(0编辑  收藏  举报