zoukankan      html  css  js  c++  java
  • 最近公共祖先(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

  • 相关阅读:
    断电数据保存问题
    强制转换的一个问题
    [LeetCode] 5. Longest Palindromic Substring 最长回文子串
    [LeetCode] 6. ZigZag Converesion 之字型转换字符串
    [LeetCode] 323. Number of Connected Components in an Undirected Graph 无向图中的连通区域的个数
    [LeetCode] 305. Number of Islands II 岛屿的数量 II
    [LeetCode] 200. Number of Islands 岛屿的数量
    [LeetCode] 727. Minimum Window Subsequence 最小窗口子序列
    [LeetCode] 76. Minimum Window Substring 最小窗口子串
    [LeetCode] 445. Add Two Numbers II 两个数字相加之二
  • 原文地址:https://www.cnblogs.com/i-love-acm/p/3556864.html
Copyright © 2011-2022 走看看