码迷,mamicode.com
首页 > 其他好文 > 详细

最近公共祖先

时间:2017-07-03 16:30:11      阅读:165      评论:0      收藏:0      [点我收藏+]

标签:osi   display   mono   qml   tao   同时存在   div   深度   and   

LCA算法

朴素算法

也就是我们所说的暴力算法,大致的思路是从树根开始,往下迭代,如果当前结点比两个结点都小,那么说明要从树的右子树中找;相反则从左子树中查找;直到找到一个结点在当前结点的左边,一个在右边,说明当前结点为最近公共祖先,如果一个结点是另外一个结点的祖先,那么返回前面结点的父亲结点即可。

class Node:
    val = 0
    left = right = None
    def __init__(self, val=0):
        self.val = val
def getLCA(current, p, q):
    left_val, right_val = p.val, q.val
    parent = Node()
    # 保证左结点的值小于右结点
    if left_val > right_val:
        left_val, right_val = right_val, left_val
    while True:
        if current.val > right_val:
            parent = current
            current = current.left
        elif current.val < left_val:
            parent = current
            current = current.right
        elif current.val == left_val or current.val == right_val:
            return parent.val
        else:
            return current.val

如果这不是一颗BST,那么同样的可以使用递归来计算,其原理是找到两个相对应的结点,如果不存在则返回空,然后向上递归,如果当前结点的左子结点和右子结点同时存在,那么说明这是最近公共祖先。

def getLCA2(root, node1, node2):
    if root == None:
        return None
    if root == node1 or root == node2:
        return root
    left = getLCA2(root.left, node1, node2)
    right = getLCA2(root.right, node1, node2)
    if left != None and right != None:
        return root
    elif left != None:
        return left
    elif right != None:
        return right
    else:
        return None

这种做法的缺点在于,每一次查询都需要重新计算,显然不适用于批量查询。

Tarjan算法

这是离线算法,离线和在线的区别在于是否提前知道它们的数据。

可以把树看作是一个有向图,在有向图G中,如果两个顶点间至少存在一条路径,称两个顶点强连通(strongly connected)。如果有向图G的每两个顶点都强连通,称G是一个强连通图。非强连通图有向图的极大强连通子图,称为强连通分量(strongly connected components)。那么这个问题就转化成了如何在有向图中查找强连通,而Tarjan算法正好是解决这个的算法。

比如在这个图中,1,2,4,5,6,7,8构成一个强连通分量,而由于39都无法达到强联通分量的相互联通的要求,因此各自单独构成一个强连通分量。

技术分享

如何理解

Tarjan算法的基本框架:

  1. 遍历一个点,指定一个唯一时间戳DFN[i],指定该点向前追溯可追溯到的最老时间戳LOW[i];
  2. 枚举当前点所有边,若DFN[j]=0表明未被搜索过,递归搜索之;
  3. 若DFN[j]不为0,则j被搜索过,这时判断j是否在栈中,且j的时间戳DFN[j]小于当前时间戳DFN[i],可判定成环.将LOW[i]设定为DFN[j];
  4. 若这个点LOW[i]和DFN[i]相等,说明这个节点是所有强连通分量的元素中在栈中最早的节点,也就是我们要找的跟;
  5. 弹栈,将这个强连通分量全部弹出,缩成点。

Tarjan算法在DFS的过程中维护了一些信息:DFN、LOW和一个栈。

  • DFN[i]:表示结点 i 在DFS中是第几个被访问到的结点,称为时间戳;
  • LOW[i]:表示结点 i 出发,通过有向边可以到达的所有结点中最小的index;
  • 栈:存储当前已经访问过,但是没有被归类为任何的一个强连通分量的结点;

算法的关键在于如何判定某结点是否是强连通分量的根,在这里根结点指深度优先搜索时强连通分量中首个被访问的结点。需要注意的是,栈中的结点不是在以它为根的子树搜索完成后出栈,而是在整个强连通分量被找到时。

void tarjan(int i)//Tarjan 
{
    int j;
    DFN[i]=LOW[i]=++Dindex;//Index 时间戳 
    instack[i]=true;//标记入栈 
    Stap[++Stop]=i;//入栈 
    for (edge *e=V[i];e;e=e->next)//枚举所有相连边 
    {
        j=e->t;//临时变量 
        if (!DFN[j])//j没有被搜索过 
        {
            tarjan(j);//递归搜索j 
            if (LOW[j]<LOW[i])//回溯中发现j找到了更老的时间戳 
                LOW[i]=LOW[j];//更新能达到老时间戳 
        }
        else if (instack[j] && DFN[j]<LOW[i])//如果已经印有时间戳 且 时间戳比较小,则有环 
            LOW[i]=DFN[j];//当前记录可追溯时间戳更新 
    }
    if (DFN[i]==LOW[i])//可追溯最小的index是自己,表明自己是当前强连通分量的跟
    {
        Bcnt++;//强连通分量数增加 
        //在当前结点之后入栈并且还不属于其它的强连通分量的结点构成以当前结点为跟的强连通分量
        do
        {
            j=Stap[Stop--];//出栈顶元素 
            instack[j]=false;//标记出栈 
            Belong[j]=Bcnt;//记录j所在的强连通分量编号
        }
        while (j!=i);//如果是当前元素,弹栈完成 
    }
}
void solve()
{
    int i;
    Stop=Bcnt=Dindex=0;
    memset(DFN,0,sizeof(DFN));//标记为为搜索过 
    for (i=1;i<=N;i++) // 确保所有结点都被访问到
        if (!DFN[i])
            tarjan(i);
}

同样的,根据程序对之前的那种图进行分析,得到最后的结果,其中每个结点上面的值代表的是该结点在有向边中能够到达的最小的index。

技术分享

其它

除了上面提到的算法,还有RMQ算法,倍增算法,后面遇到再更新了。

参考

参考1参考文章2

最近公共祖先

标签:osi   display   mono   qml   tao   同时存在   div   深度   and   

原文地址:http://www.cnblogs.com/George1994/p/7111274.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!