LCA,Lowest Common Ancestor,最近的公共祖先。在一棵树中对于两个节点u , v找出节点T,使得T同时为u,v的祖先。显然这样的T点肯定存在且有可能有多个,其中深度最大的那个点肯定为即为u,v两点的LCA。关于LCA的解法有很多种,暴力枚举,事先需要知道所有询问的离线的tarjan算法和基于RMQ的在线算法,下面说一下自己对这种几种算法的理解。
⒈最容易想到的暴力搜索。
给出节点u , v,,首先对u进行回溯一直到根节点,并对途中的节点加上标记。然后对v进行回溯,直到找到一个被标记的节点T,此时T即为u,v的LCA。此方法写起来很简单但时间复杂度太高,故只适合查询次数极少的时候。
⒉离线的Tarjan算法。
此种算法需要预先知道所有的询问,并对询问进行一些预处理,即将有相同节点的询问放在一块。算法的主要思想就是在DFS过程中处理一些信息从而得到答案。下面说一下我自己的理解。
在DFS进行之前,把每个点都看作一个独立的点集且作为对应点集的代表元。在DFS过程中每次遍历完一棵子树,回溯到当前子树的根节点时,便将这棵子树上的所有点并到一个点集里,根节点即为该集合的代表元。然后对与此根节点相关的询问进行回答。设此节点为u,另一点为v,若v已经完成DFS,则v所在点集的代表元即为u,v的LCA。
下面说一下细节:1.点集的标记可以用并查集来完成。2.每次询问都查询了两次,并且有且只有一次做出回答。3.此算法的缺点在于必须知道预先知道所有的询问,不够灵活。
⒊基于RMQ的在线查询算法。
该算法借助于DFS时的访问次序和RMQ的快速查询。
设depth[]记录每个点的深度,r[]记录每个点在DFS过程中第一次被访问的次序。
刘汝佳的黑书上对此算法作了详细的介绍,当时读的时候有一句“由于每条边被访问了两次,因此一共记录了2n-1个节点”始终想不明白,后来发现在DFS的递归和回溯过程中,对于一个出度为X的点,肯定会被访问X+1次,1次在递归过程中,X在回溯过程中。又因为在一棵树中除根节点外,每个节点的入度均为1,故入度总和为n-1,又有入度 == 出度,所有所有的节点一共被访问了n+n-1次,n次在递归过程中,n-1次在回溯过程中。
设r[u] < r[v] , point[2n-1]里面存放了DFS过程中一次被访问的节点。则在point[ r[u] ]到 point[ r[v] ](包括两端点)之间深度最小的那个点即为两点的LCA,深度最小的点有且仅有一个。此时可分为两种情况,一种是u即为u和v的LCA,第二种就是第三点w是u,v的LCA。
第一种情况中显然v在一棵以u为根节点的子树上,此时显然在DFS过程中先u先放入point[],然后在DFS遍历这个子树的过程中v放入point[],所以在[ r[u] , r[v] ] 中 u即为深度最小的那个点。
第二种情况中DFS过程中必先会遍历完其中一个点所在的子树,然后会回溯到某一节点w继续DFS遍历该节点的其他子树,如果v在此时遍历的子树上,则w即为u,v的LCA。由上可知,point[]的[ r[u] , r[v] ] 区间内必有w。
区间内的快速查询可以用到RMQ或者线段树,这里就不再赘述。
以上就是我对LCA的几种算法的理解,感觉说的有点罗嗦,不足之处还望指出。