LCA问题第二弹
LCA问题第二弹
上次用二分的方法给大家分享了对 LCA 问题的处理,各位应该还能回忆起来上次的方法是由子节点向根节点(自下而上)的处理,平时我们遇到的很多问题都是正向思维处理困难而逆向思维处理比较容易,LCA问题也可以划分为这一类问题的范畴。那是不是就意味着 LCA 无法从正面思维中解决呢?当然不是的,只是要直接想到解决的办法需要耗费一些功夫。那今天咱们就从问题的正面来研究一下 LCA ,也就是说,今天我们采用由上而下的遍历方式处理 LCA问题,那今天我们的目的能够达到吗?且往下看。
由上而下势必要对树进行遍历,树的遍历大致分为四种类型:先序遍历(先根遍历)、中序遍历(中根遍历)、后序遍历(后根遍历)、层序遍历。
先序遍历:对于一棵(子)树,先遍历根节点,再遍历左子树,最后遍历右子树的遍历顺序。
中序遍历:对于一棵(子)树,先遍历左子树,再遍历根节点,最后遍历右子树的遍历顺序。
后序遍历:对于一棵(子)树,先遍历左子树,再遍历右子树,最后遍历根节点的遍历顺序。
层序遍历:对于一棵(子)树,按照树的层级结构对所有节点进行遍历。
如下图:
先序遍历序列为:A B D H P I Q T E J K R C F L S M G N O
中序遍历序列为:P H D I T Q B J E R K A L S F M C N G O
后序遍历序列为:P H T Q I D J R K E B S L M F N O G C A
层序遍历序列为:A B C D E F G H I J K L M N O P Q R S T
假设我们还是求 P 和 D 的最近公共祖先 D ,观察先序遍历、中序遍历、后序遍历的遍历路径和序列,我们很难整理出可以转化为代码实现的思路来,关键在于这三种遍历方式在对节点的访问上跳跃性太大,无法连续的访问整棵树,而这种跳跃性在序列上是没有表现出来的,因此很难利用。
再看层级遍历,虽然也不是连续的,但是其层级的特征十分明显,可以适当运用。提一个大家都想得到的实现方法:对层序遍历函数进行改写,加入检测函数用来检测当前节点是否就是最近公共祖先,当在某一子树中发现两个待求子节点时,将不必再遍历同级剩余未遍历的子树了。检测函数实现步骤核心如下:
以当前所在节点为根节点向子树进行遍历,若能够同时找到两个待求子节点,将当前节点更新为可能的最近公共祖先并访问下一级,如果下一级的任何一个子树都无法同时找到两个待求子节点,那么确定之前的最近公共祖先即为所求,如果下一级中存在某一子树同时包含两个待求子节点,更新此子树的根节点为可能的最近公共祖先,进行同上一级同样的处理,具体实现代码不做赘述。
由上面的描述来看,似乎 DFS 更适合来实现检测函数,因为在一个分支找到想要的结果之后不用再耗费时间确认其他分支,这样更节省时间。而如果所求子节点是叶节点, BFS 可能需要遍历所有的节点才能知道结果。
现在面临的问题只是求两个子节点的最近公共祖先节点,当问题的规模逐渐增大的时候,以上的方法显然捉襟见肘,因此我们需要深入分析。
上一个方法主要的时间耗费在对树的遍历上,如下图所示:
若要求 X 和 Y 的最近公共祖先,那不管是采用BFS的方法还是 DFS的方法,显然需要遍历的次数都远远难以接受。如果对于一棵树,进行 n 次遍历,待求子节点都在如图 X 和 Y 的位置,那样的复杂程度更是难以接受的。因此我们希望减少遍历的次数,甚至一次遍历便可以解决问题,哪怕是遍历过程中需要牺牲空间来存储数据也是可以考虑的,因为我们擅长的是对数组的操作。
这时候我们考虑放弃不连续的 BFS 采用 DFS 进行遍历,因为 DFS 能够保证序列在树中的连续性,也方便多次利用。因此我们首先需要建立一个 visit[]数组存储整个遍历过程中访问到的点,这个数组究竟开多大呢?假设树中有 n 个节点。那么 visit[]数组的大小一定是 2n-1,为什么是 2n-1?请看下图:
这棵树的节点数为7, DFS 访问序列为 A B D B E B A C F C G C A ,共访问过 13 个节点。大家会告诉我,这棵树已经画到这里了,当然能数出来访问了几个点,如果没画出来只告诉节点总数,那怎么判断,还会这么准吗?模仿 DFS 过程,我们会发现正好每两个节点之间连接的线段(后面我将称之为 “枝干”)都被经过了两次,不论是在 DFS 深入的过程中还是回退的过程中,每个枝干总是顺着前进的方向向前对应一个目标节点,也就是说经历过多少次枝干,就访问过多少个节点,在一棵 n 个节点的树中,枝干的数量为 n - 1,因此,在一次 DFS 过程中,经过 2 *(n-1)次枝干,也就访问过 2*(n-1)个节点,但刚才我说过 访问的节点数是 2n-1,怎么还少了一个呢?仔细观察树的图就会发现,除了根节点,其余子节点都有一个由父节点指向自己的枝干,由于根节点无父节点,因此没有指向自己的枝干,在开始访问一棵树的时候没有枝干的引导也会默认访问到根节点,因此忽略掉的那次便是开始访问根节点的那一次,由此可以断定,visit[]数组一定是包含 2n-1个元素。
对树的遍历和存储已经完成,接下来如何实现在 visit[]数组中进行最近公共祖先的查找。我们在进行 DFS 遍历的过程中不难发现,在进行待求两个子节点之间的那部分节点遍历过程中,能够到达的层数最小的节点就是所求的最近公共祖先节点。直接上图可能更直观一些,如下丑图:
上图中所画出的路径是 DFS 遍历路径的一部分,路径上伸出的触角(小箭头)指向的是在相应位置的时候访问的节点。我们假设现在需要求解 T 和 J 两个节点的最近公共祖先 B ,由图可知蓝色路径部分即为 T 和 J 两节点之间的 DFS 路径,我们会发现 B 节点恰好就是处在整段蓝色路径上的所有节点中层数最小的一个。这个规律同样也适用其他任意两个节点。我们知道visit[]数组和 DFS 访问 的顺序是一致的,因此我们就可以从 visit[]数组的相应区间寻找层数最小的节点,这就要求我们对应 visit[]数组,开辟同样大的数组 level[]来存储每个节点对应的层数。还有一点需要确定就是相应的区间范围,为避免选的区间过长同时保证万无一失,我们从两个待求子节点首次出现的位置截取区间,因此需要将每个节点首次出现的序号也存储。
具体的代码实现部分如下:
由于 今天的主题是 LCA ,因此代码部分实现 RMQ 的过程被省略了,还没有看过这部分知识的朋友可以查看历史消息,了解 RMQ 问题的详细知识,(RMQ的链接在这里呦:RMQ问题第一弹)在此不做过多的赘述,图片不够清晰的朋友打开网页http://paste.ubuntu.com/25407878/查看网页版的代码。