LCA,最近公共祖先,实现有多种不同的方法,在树上的问题中有着广泛的应用,比如说树上的最短路之类。

LCA的实现方法有很多,比如RMQ、树链剖分等。今天来讲其中实现较为简单的三种算法:RMQ+时间戳、树上倍增(类似二分步长)、Tarjan算法(DFS+并查集)。
【RMQ+时间戳】
什么是时间戳?时间戳,就是被访问到的一个次序。比如说我们首先对一棵树进行深搜,在深搜中访问的相应次序就被我们称为时间戳。比如说对下面这棵树进行相应的深搜,我们得到时间戳,以及相应的遍历序列:

这棵树所得到的DFS遍历序列就是1 2 3 2 4 2 5 2 1 6 7 8 7 6 1。可以看出,每个非叶节点都被访问了不止一次。好了,时间戳讲完了,接着转入正题:怎样用RMQ?RMQ在这里是我们算法的一个极为重要的基础,具体有什么不明白可以看我以前的博客《Sparse-Table算法 - 一类RMQ问题的简单高效解法 》。
既然我们得出了这么一个序列,要RMQ,就是对这一个序列进行。而这个思路是正确的。如果我们对于每一个节点,都只记这一个节点在DFS序列中第一次出现的位置,比如说节点1,第一次就是在位置1,节点6,第一次就是在位置10,以此类推。然后我们对这么一个DFS序列作RMQ的最小值预处理,然后对于LCA的每次询问,只需求这两个被询问节点在DFS序列中第一次出现的位置构成的区间内的最小值即可。时间复杂度加上预处理,应该是O(Nlog2N+Qlog2N)的时间复杂度(虽然说实际上来说RMQ的单次询问是和O(1)相当的,但实际上的规模是O(log2N),如果另外处理一个常数表,占用空间可能挺大的,毕竟直接实现的速度也挺快的)。举个例子,我们要询问节点6和8的LCA,则节点6的首次出现位置是10,节点8的首次出现位置是12,则区间[10,12]中的最小值是6,那么他们的LCA就是节点1。
那么,为什么这样会可以呢?考虑一下我们深搜的过程,寻求两个节点的LCA朴素过程其实可以转变成这样的形式:从其中一个节点出发,一直往上找,并不断遍历以当前找到的结点为根的子树,看有没有同时包含两个节点。如果第一次找到有同时包含两个节点的子树,则这棵子树的根就是LCA。也就是说,相当于下面的过程:
1.输入两个节点A和B2.设当前节点为A3.while dfs(当前节点)没有搜索到A和B4.当前节点更新为当前节点的父节点5.输出当前节点

很容易看出这个过程的正确性。首先,LCA的子树中必定有一个节点是A,一个是B,而且必定在两个节点到根节点的唯一路径上。

因此,我们可以引出定义:某两个节点的LCA就是其在DFS遍历整棵树时同时访问到这两个节点的“回溯最近节点”。而且,在刚刚遍历完这两个节点时,它们的LCA必定没有回溯!因此,我们就通过了时间戳做到了这一点。某两个节点的DFS间所访问到的哪些节点中的最小值必然是这两个节点的LCA。
【树上倍增(类似于二分步长)】
对于下面的这棵树,我们先随意指定一个编号:

然后,我们对于每一个节点,记录其往上跳1个节点,2个节点,4个节点,8个节点,16个节点……2^k个节点所达到的节点编号。在这里,往上跳2^k个节点应达到的是节点-1,也就是说,向上跳2^k个节点是不可能的,但是跳2^(k-1)个节点应该可以跳到。这可以用链表存储,也可以直接开一个数组,以方便索引。

那么这个表怎样建立呢?难道对于每一个节点都要一直用while循环一直找到根节点吗?不是的,我们只需要稍加分析,就可以写出一种总时间复杂度O(Nlog2N)的优秀算法。
首先,每个点向上跳一个点,必定是其父节点,根节点就是-1。然后,显而易见的,向上跳2^i个节点,等价于先向上跳2^(i-1)个节点,再在向上跳的基础上再向上跳2^(i-1)个节点。当然,这要求向上跳到的节点的表已经算过了。于是,我们需要使用DFS,以保证这一次序。简单分析一下就可以得出最多往上跳log2N层,而一共有N个节点,乘在一块就是O(Nlog2N)。具体的实现如下:
1.DFS(根节点),对于每个DFS到的节点(不包括回溯到的): 2.设当前跳到的节点为DFS的目前节点的父节点  3.设当前向上跳2^i个节点,其中i=0 4.while 当前跳到的节点仍然存在(即不为-1)     5.当前节点的表中加入向上跳2^i跳到当前跳到的节点       6.++i      7.当前跳到的节点更新为当前跳到的节点向上跳2^(i-1)的节点

其实这一种算法相当于在树上添加了一些额外的边,有点像自动机的结构,就像AC自动机一样都和树(AC自动机实际上是添加了状态转移边的Trie树,或者说字典树)有关。我个人觉得这其实是一种双方向的状态转移,应该比较特殊。
回归正题,继续讲倍增算法。LCA不是每一次询问都要给两个点的编号吗?我们就设这两个点为点A和点B。首先,如果这两个点的深度不同(深度可以顺便在DFS时求出),则先将较深的一个节点向上一直跳,跳到和另一个点相同的深度,这可以用我们刚才所造的表,并用二进制的lowbit优化,实现如下:
1.设要向上跳k层,当前跳到节点为A'2.while k != 0  3.A'向上跳lowbit(k)层   4.k -= lowbit(k)

接着相应的将相应的节点设为A’,如果A'和另一节点相同则LCA就是那个节点。于是,下面的思路就也很明朗了。二分LCA!比如说现在A和B在同一层上(深度相同),并且设深度都是100的话,就先向上跳64层,如果相同就只跳32层(在原来节点的基础上),不相同则再跳32层(在跳64层达到的两个节点的基础上),最后即可轻而易举地二分到答案了……这一过程似乎又称树上倍增算法,正确性的证明很简单,那个表的过程就不说了,而LCA的最终二分过程应该类似于二分步长,不断试探,最终必定能够找到答案。

最终的时间复杂度,由预处理和二分答案的过程可知,为O(Nlog2N+Qlog2N),和RMQ相当。
【Tarjan算法:并查集的离线算法】
前两个算法,都是在线算法,都是可以在线处理询问的,但是在信息学竞赛中,因为是黑箱测试,我们也可以采取一种时间复杂度更为优秀的算法:Tarjan。这种算法是一种 离线的算法,要求事先给出每一个询问,而处理的时候不一定按问题的给出顺序回答。
Tarjan算法基于一个和RMQ那个章节中最后一部分所陈述的那些东西一样的事实。也就是说,当我们从根节点开始DFS时,我们在 刚刚好访问 两个节点时,这两个节点的LCA 必定没有在DFS中回溯。
所以,我们设back[x]为点x回溯到的节点标号。初始时设back[x]=x,接着在DFS的过程中依次更新。
比如说,我们当前遍历完了点x的所有子树,那么我们就可以设定:back[x]=father[x](father[x]表示x的父亲节点)。以此类推。
然后,我们将LCA每次询问的一个节点对称为一个问题,一个节点是某个问题的节点对两个节点中的一个,则称之为这一个节点涉及到了某个问题。当我们DFS到一个被某个(或者说某些,情况都是一样的)问题涉及的节点时,如果该问题的另外一个节点没有被访问,则什么都不做,如果曾被访问,则对当前节点x执行以下的操作(当然首先要设back[x]=father[x]):  
while (x!=back[x]){    x = back[x];}

最后得出来的x,就是LCA!于是将LCA相应地回答那个问题。那么,怎么用并查集呢?   注意到刚刚那段代码了没有,那段代码和并查集的代码是否有几分相似?所以,我们引入路径压缩!
int findLCA(int x){    if (x==back[x]) return x;  return x = findLCA(back[x]);}

由并查集的时间复杂度可知,总的时间复杂度应该是O(N+Q* α(N) )。而且,这里的应用还不需要合并,只需要查询即可,写起来比普通的并查集还要简单。

【总结】
这三种方法,在信息学奥赛中,可谓是非常实用的算法。今年NOIP2015提高组的最后一题,就有这样的思想在里面。因此,在树与图这样的东西里,LCA几乎就是家常便饭了。所以,我们必须记住这些算法的原理和具体的实现。

LCA实现的三种不同的方法相关推荐

  1. 【小白学习keras教程】十、三种Model Selection方法:k-fold cross-validation,GridSearchCV和RandomizedSearchCV

    @Author:Runsen Model Selection是划分训练集和测试集的手段,下面总结了三种Model Selection方法. k-fold cross-validation using ...

  2. Python实现二叉树的三种深度遍历方法!

    python代码实现了二叉树,这次将会实现二叉树的几种遍历方法,来更好的解析二叉树的结构特点.分别是一种广度遍历,和三种深度遍历方法:先序遍历,中序遍历,后序遍历.下面是代码实现: 1.先序遍历 遍历 ...

  3. php调用mysql库_PHP调用三种数据库的方法(1)

    PHP调用三种数据库的方法(1) 更新时间:2006年10月09日 00:00:00   作者: MySQL是一个小巧灵珑的数据库服务器软件,对于中.小型应用系统是非常理想的.除了支持标准的ANSI ...

  4. 实现两数 交换的三种不同编程方法。

    第一种 即常规方法 借助第三变量 int a = 5,b = 3,c; c = a; a = b; b = c; 第二种 不借助第三变量 int a=5,b=3,c; a = a+b;    //(缺 ...

  5. 常用的分隔符有哪三种_掌握这三种调漂方法,你想怎么钓就怎么钓,再也不用求人...

    调漂对于刚学钓鱼的钓友来说是一件非常头痛的事情,每次钓鱼大部分时间都浪费在调漂上,总是感觉调不好,不是灵了就是钝了!那么问题到底出在哪呢?今天就和大家分享三种针对悬坠钓的调漂方法,看懂弄明白了,再也不 ...

  6. Python 三种读文件方法read(), readline(), readlines()及去掉换行符\n

    Python 三种读文件方法read(), readline(), readlines()及去掉换行符\n 首先, 让我们看下数据demo.txt, 就两行数据. 35durant teamGSW 1 ...

  7. OpenCV函数应用:基于二值图像的三种孔洞填充方法记录(附python,C++代码)

    系列文章目录 函数系列: OpenCV函数简记_第一章数字图像的基本概念(邻域,连通,色彩空间) OpenCV函数简记_第二章数字图像的基本操作(图像读写,图像像素获取,图像ROI获取,图像混合,图形 ...

  8. 极低噪声幻像电源如何设计?详细原理图和三种消噪方法拿走不谢

    极低噪声幻像电源如何设计?详细原理图和三种消噪方法拿走不谢 原创 ADI 亚德诺半导体 2022-04-19 11:48 极低噪声幻像电源如何设计?详细原理图和三种消噪方法拿走不谢 Q: 是否可以利用 ...

  9. 分享三种高效的方法,快速将一个PDF文件分割成两个!

    如何将一个PDF分割成两个?PDF文件在学习和工作中都是常用的文件格式,但是有时候我们可能只需要其中的一部分内容,这时候就需要将PDF文件分割成两个或多个.本文将分享三种不同的方法,供大家参考. 一. ...

最新文章

  1. mysql索引为啥要选择B+树 (上)
  2. TextBoxSuggest,输入框提示工具,输入建议,输入匹配,辅助输入,输入即时提示,文本编辑器,Visual Studio效果,高速查询引擎,哈希树,模糊匹配,百万条零毫秒
  3. Unity插件之NGUI学习(8)—— Table和NGUI尺寸转换为世界坐标系尺寸
  4. msf principle
  5. 在全局中谋一域_谋全局才能谋一域
  6. 英语语法---分词短语详解
  7. Android Studio快捷键之代码提示
  8. 敏捷开发系列学习总结(6)——你用什么工具管理项目
  9. 计算机应用与维修专业主修,计算机应用与维修专业个人简历制作
  10. GBDT 算法:原理篇
  11. Mybatis常见配置错误总结
  12. ubuntu14.04 clementine音乐播放器无法播放ape格式解决方法
  13. 七夕到了 —— 属于 Java 的浪漫,拿去吧~ 祝表白成功
  14. uniapp的navigateTo页面跳转参数传递问题
  15. 银行系统日终结算要多久_银行 核心系统的清算与结算 - 系统性能与软件架构 - 51Testing软件测试网 51Testing软件测试网-软件测试人的精神家园...
  16. C++小白如何做简单游戏
  17. TOJ 3778.Sheldon's Friendship II
  18. [应用模板]HTML5电子相册
  19. 求一个n阶方阵对角线元素之和
  20. mfc设置半透明对话框

热门文章

  1. CSAPP Architecture Lab PartC满分
  2. 【Druid】Druid连接池(二)——配置监控界面
  3. OGRE 引擎官方基础教程 (一)
  4. 如何配置和测试ChatBot
  5. async、await
  6. 防火墙规则应用同一子网计算机,防火墙基础(一)
  7. 查找书籍!!!查找书籍!!查找书籍!
  8. 数据挖掘与机器学习——离群点检测之孤立森林(isolate forest)
  9. hfs支持php文件系统,HFS+文件系统的发展及特点介绍
  10. Unity云渲染开源框架UnityRenderStreaming使用说明