【白话系列】最近公共祖先

来源:互联网 发布:ubuntu软件安装在哪里 编辑:程序博客网 时间:2024/05/17 10:42

【序言】

        说到最近公共祖先,应该是树论中一个比较重要的话题吧。一般来说,在遇到求最近公共祖先的时候,会有三种常见的做法:对于简单的模拟题——直接模拟就好了;对于大题目中的求最近公共祖先的小桥段——用tarjan来求,因为好打不容易错;对于特意考察最近公共祖先,并且数据范围比较大的时候——用倍增算法,省空间还是硬道理。至于还有的通过变形将最近公共祖先问题化为区域最小值问题来做,性价比并不高,如果你硬是想知道,可以百度一下:“LCA问题转RMQ问题的ST算法”。


【什么是最近公共祖先?】


        最近公共祖先简称LCA,以下用LCA代替。

        不要期望我解释什么是LCA嗯,我知道你知道!(哼,这只是走个过程而已!)好吧,如果你真不知道,我也无法解释,请看:

        LCA3 4=2   LCA3 2=2    LCA6 10=1   LCA5 6=4

        我想你已经知道了,LCA就是两个节点前往根节点的两条路径第一次交汇的那个节点,也就是距离它们最近的祖先,而且是公共的祖先,哈哈!


【模拟的做法】

        还记得刚才的那句话么!“LCA就是两个节点前往根节点的两条路径第一次交汇的那个节点”!那么模拟法岂不是太显而易见了吗?直接从要求的一个点开始,不停地往父亲走,把它经过的点都标记为已访问,直到不能再走为止,再从另一个点开始,不停往父亲走,并检查它经过的点是不是曾今被访问过,如果是,那么这个点就是它们的最近公共祖先。如果你要问我为什么,我真的会很难过的,真的。

        注意:模拟法在马虎的时候也是容易出错误的,记住一个完整的小流程是“先标记再往上走”而不是“先往上走再标记”,这并不一样,如上图,若是找与 3LCA,先模拟2的路径,如果“先标记再往上走”那么走完以后被标记的有12,如果“先往上走再标记”,那么被标记的就只有1,显然这是不可取的,因为最后求出来的LCA就变成1号节点了!这是常见的一个小错误,当然,对于另外一个节点,也应该“先检查再往上走”,因为它自己本身这个节点就有可能是它们的LCA。切记啊切记,这样的错误不能出现了啊!!


【tarjan的做法】

        刚才我们一直在做的都是解决两个节点的LCA是哪个节点,tarjan固然也是解决这样的问题的,只不过它可以更加快速,在线性的时间阶内求出所有的询问。tarjan到底是怎么做的?请往下看。


        首先,我们来想想这样一个问题:在如图的这棵树中,LCA1号节点的有哪些节点对?也许你觉得这个问题实在是太简单了,一眼就可以看出,只要在1号节点的左子树随便找一个节点,再与从1号节点的右子树中随便找出的一个节点组成节点对,那么它们的LCA一定是1号节点。为什么?显然可得,不需要任何理由,感觉就是硬道理。

        那么我们可不可以抽象一样:若两节点分别分布于某节点的左右子树,那么该节点为其LCA。凭感觉得出的定理还是有一定的问题,因为并没有考虑到一个节点自己就是LCA的情况,所以我们对定理进行补充:若某节点是两节点的祖先之一,且这两节点并不分布于该节点的一棵子树中,那么该节点即为两节点的LCA。这就是Tarjan算法赖以生存的基础。

        先不说Tarjan算法,就说刚才我们得到的那个显而易见的定理,你有没有什么思路呢?你有没有想到,可以先预处理出所有询问的LCA,然后再一起回答呢?

        对于很多组的询问,我先确定一个LCA,就假设它是根节点1好了,然后再去检查所有询问,看是否满足刚才的定理,不满足就忽视,满足就赋值,全部弄完,再去假设2号节点是LCA,再去访问一遍……有没有发现这个方法无比的通俗与直观?但是!你要怎么知道一个节点是在左子树、右子树还是都不在呢?我想你只能遍历一棵树,那么,好像这个方法也并没有比直接模拟法好多少,但是,不要放弃,因为Tarjan就没有放弃。

        我们觉得刚才的算法不妥,是因为多次遍历的代价实在是太大了,但是细心一点,我们便可以发现,若一个点的父亲会被某个点遍历到,那么该点也会被那个点遍历到,也就是说一个点只需要被遍历一遍即可,因为遍历信息是可以传递的!

tarjan算法流程:

        procedure dfsi);

        begin

            设置i号节点的祖先为i

            若i的左子树不为空,dfsi-左子树);

            若i的右子树不为空,dfsi-右子树);

            访问每一条与i相关的询问

                    若另一个节点已经被访问过,则输出另一个节点当前的祖先

            标记i为已经访问,将所有i的孩子包括i本身的祖先改为i的父亲

        end

STEP 1

节点

1

2

3

4

5

6

7

8

祖先

1

2

3



STEP 2

节点

1

2

3

4

5

6

7

8

祖先

1

2

2


STEP 3

节点

1

2

3

4

5

6

7

8

祖先

1

2

2

4

5



STEP 4

节点

1

2

3

4

5

6

7

8

祖先

1

2

2

4

4

STEP 5

节点

1

2

3

4

5

6

7

8

祖先

1

2

2

4

4

6


STEP 6

节点

1

2

3

4

5

6

7

8

祖先

1

2

2

4

4

4


STEP 7

节点

1

2

3

4

5

6

7

8

祖先

1

2

2

2

2

2


STEP 8

节点

1

2

3

4

5

6

7

8

祖先

1

1

1

1

1

1


STEP 9

节点

1

2

3

4

5

6

7

8

祖先

1

1

1

1

1

1

7


STEP 10

节点

1

2

3

4

5

6

7

8

祖先

1

1

1

1

1

1

7

8


STEP 11

节点

1

2

3

4

5

6

7

8

祖先

1

1

1

1

1

1

7

7


STEP 12

节点

1

2

3

4

5

6

7

8

祖先

1

1

1

1

1

1

1

1


STEP 13

节点

1

2

3

4

5

6

7

8

祖先

1

1

1

1

1

1

1

1


        大致流程如上所示,我们可以惊喜的发现,当我们在检查一个节点的询问情况的时候,若与询问相关的另一个节点已经被访问,那么以另一个节点当前的祖先为祖先,这两个节点一定是满足我们凭感觉得到的那个定理的,也就是说,这个祖先一定是最近公共祖先。

        为什么?因为这个神奇的逻辑顺序,就是这么这么巧,没有任何问题。

        如果你还是有点懵懂,按照Tarjan的算法流程再将这十来幅手动模拟的图片看上几遍,你一定就会懂的。


【倍增的做法】

        倍增来做LCA应该是比Tarjan更容易理解的,因为它更加直观,更加符合人模拟的思维。

还记得前面说的模拟的方法来做LCA吗?其实倍增可以算作是模拟算法在往上走的过程中的一个优化,让我们不是每次走一步,而是尽可能一次走很多步。

        ps、倍增是什么?详情请看http://blog.csdn.net/jarjingx/article/details/8180560 

        既然已经知道了倍增,那么就不赘述了,直接上算法流程。

        1、预处理出每个节点的深度

        2、读取一组询问,对于两个节点,先跳到同一深度

        3、判断当前两节点所在的节点是否为同一节点,是则其为LCA,否则继续下一步

        4、从大往小进行检查,……8步、4步、2步、1步……,若跳后节点不一致,则可以跳,若节点一致,则不跳

        5、两节点所在的点的父亲节点即为LCA

若询问为 611LCA

        step1、比较深度大小


        step2、深度不一致,跳至同一深度


        step368步与98步不满足要求

                       64步与94步步满足要求

                       62步与92步满足要求


        step421步与71步不满足要求

        step527共同的父亲1为其LCA,输出结果

        观察倍增算法在树上的实现,我们发现其实跟兔子跳格子是一样的,从每个节点跳几步会到哪个节点是需要我们预处理出来的,方法就跟聪明小白兔晚上打小抄的方法一致,在真正跳的时候,也跟聪明小白兔的方式一致。

        也许,现在你更加明白倍增算法最后的那段话了,从一个节点,若想往上跳2步,在没有预处理的情况下你只能11步的跳,因为你只能知道当前节点的父节点是谁,而无法知道爷爷节点是谁。


【尾声】

        LCA其实是个特别好玩的东西,很多在树结构中难以想到的东西都或多或少可以用到LCA的思想来工作,更多神秘的东西就等待你去发现啦。

                                                                                                                                               

原创粉丝点击