LCA问题



LCA问题第一弹

上篇文章讲到 区间最值 RMQ 问题,今天,我们来研究一下 LCA 问题。

LCA( Least Common Ancestor)问题:中文名为“ 最近公共祖先”问题。LCA问题定义是这样的:在一个树形结构中,求解两个子节点的公共祖先中离根节点最远的那个祖先节点,换言之,分别从两个子节点开始向根节点遍历,两条路径最早交汇节点就是最近公共祖先。

为了给大家更加形象的展示一下LCA到底是什么,我用尽毕生力气画了一棵非常不美观的树,希望大家摒弃丑陋的外表来看这棵树。示例如下:

例一:求 P 和 T 的最近公共祖先

第一种理解:P 和 T 的公共祖先为 D 、B、A,其中离根节点最远的公共祖先为 D

第二种理解:P 到根节点的路径为 P->H->D->B->A , T 到根节点的路径为 T->Q->I->D->B->A ,由此可知 最早交汇点为 D,因此最近公共祖先为 D

例二:求 P 和 R 的最近公共祖先

按照例一的分析方法,最近公共祖先为 B

例三:求 P 和 S 的最近公共祖先

按照上述方法,最近公共祖先为A

知道了 LCA 的基本概念,那我们来研究一下如何求解的问题。

想必大家已经从上面的描述中看出了端倪,没错,上面给大家分析那棵颜值低的树时,已经很直接的使用了两种思维。分析一下:最近公共祖先是两个待求子节点所有公共祖先中离根节点最远的,如果根节点定义为第一层,其子节点定义为第二层,那么也就是要求得两个待求子节点的所有公共祖先节点中层数最大的,同时也是两个待求子节点到根节点路径的最早汇合点。

我们都知道一般的,我们存储树结构时,通常对于一个节点,只存储其子节点的指针(地址),并不会存储其父节点的指针(地址),这就意味着从一个节点开始,我们只可能得知该节点的子节点有哪些,而无从得知其祖先节点都有哪些,或者是,依据当前的存储结构得知祖先节点的复杂程度比较大。此时,对于如何求解某一确定位置的子节点的所有父节点,我们可以考虑将树结构存储成既有指向子节点指针也有指向父节点指针的,也可以选择另辟数组单独存储各节点的父节点。如此求出待求解两个子节点的所有祖先节点进行对比,找出离根节点最远的一个,那这个远的定义就体现在层数大了。当然在存储树的时候,需要再增加一个纪录当前节点所在层数的量。由于我们在求解两个子节点的祖先节点时,必定是由下而上(由子节点向根节点寻找)当我们找到第一个公共祖先节点时,实际就已经找到了答案,因此在实际求解中,我们通常求到第一个公共祖先节点就终止。

在展示代码部分之前,有一个现象值得我们注意:对于同一棵树的两个子节点 X 和 Y 来说,如果 X 的层数比 Y 的层数小,那么 Y 的所有祖先中,和 X 处在同一层级的祖先节点 Z 满足 X 和 Y 的最近公共祖先节点与 X 和 Z 的最近公共祖先节点相同。那么我们就可以从 X 和 Z 所在的层级开始逐级向上检验两者的祖先节点是否相同,一旦相同结束程序。

下面给大家分享一下本方法的核心代码,由于在实际问题的求解中,不一定就是二叉树的LCA 问题,因此在这里采用图的存储方式进行展示。

看了上面的代码展示,大家一定已经彻底理解了 LCA 问题的概念,分析以上代码可以得知在将两个子节点等效处理到同一层级的过程中以及将同一层级的两个子节点向上遍历找共同祖先的过程中,我们采用的都是逐个遍历的方法,这样的遍历方法其实是最容易想到的,也是比较费时间的,我们当然希望能够找到一种方法,更加精准的寻找公共祖先,这时候就需要来仔细的研究一下需要遍历的数据特征。不难发现寻找公共祖先的本质是在父节点编号组成的 father[]数组中查找满足条件的一个元素。对于区间上的查找问题,除了逐个遍历,还有一个比较经典的就是二分查找。二分查找的条件是将区间一分为二之后一定有一半区间里的值均不符合条件。回过头来再想一想树的特征:两个子节点向根节点遍历的两条路径一旦汇合便不会再分叉,也就意味着如果 father[]数组是从根节点到子节点的存储顺序,将区间一分为二,若中间值是两个待求子节点的祖先,则前半区间一定都是两个待求子节点的祖先,最近公共祖先一定会出现在后半部分区间;若中间值不是两个待求子节点的公共祖先,则后半区间一定都不是两个待求子节点的公共祖先,最近公共祖先一定会出现在前半部分区间。

因此在从同一层级向公共祖先这部分的查找过程我们就可以采用二分查找的方法。在描述第一种方法的时候我就曾提过一个现象:对于同一棵树的两个子节点 X 和 Y 来说,如果 X 的层数比 Y 的层数小,那么 Y 的所有祖先中,和 X 处在同一层级的祖先节点 Z 满足 X 和 Y 的最近公共祖先节点与 X 和 Z 的最近公共祖先节点相同。从这个现象来看,由 Y 寻找到 Z 的这个过程其实是没有值得关注的点的,因为在这个过程中,我们只关心 Z 的位置和编号,至于中间过程中的那些祖先都是谁我们丝毫不关心,这时候,我们希望有一种方法能够直接一针见血的找到 Z ,只有这样才能节省不必要的时间开支,如果不能达到,至少我们可以简化这个寻找 Z 的过程。如果这时候,我们有张表,存储了每个子节点的 i 辈祖先是谁,那就堪称完美了。

大家可能都注意到了,不仅 Y 到 Z 的过程中可以用的到这个表,X 和 Z 求公共祖先的二分过程中也是必须用的。既然两个重要的环节都得用到,不妨我们就舍出这段磨刀的功夫,毕竟实际求解问题的过程中不一定就只是求两个子节点的公共祖先,万一题目比较丧心病狂让你挨个求个遍,那有了这个表真是省事不少呢。

不知道大家还能不能回忆起 RMQ 问题,那个时候我们也是建了个表呢,隐约觉得那个建表的方法可以直接拉过来套用。不管用不用得上我们先试一下,万一就成了呢?

首先我们来研究一下隔好几辈的 father 怎么求。

假设 子节点编号为 i ,那么它的第一代父节点就是 father1[ i ] = father[i] , 它的第二代父节点就是father2[i]= father1[ father1[ i ] ] ,它的第三代父节点就是father3[i] = father1[father2[i]],它的第四代父节点就是 father4[i] = father2[father2[i]]由此往后推理就可以推出所有父节点。当然同 RMQ 问题中需要注意的问题一样,我们需要先求出所有节点的第一代父节点,才能求所有节点的第二代父节点,这样的求解顺序不能颠倒。

接下来我们按照动态规划的思想来定义 dp 数组,设 dp[ i ] [ j ]  表示 编号为 i 的节点的第 2 ^ j  代父节点。依据上面的推理关系,状态转移方程如下:

dp[ i ][ j + 1 ] = dp [ dp[ i ][ j ] ][ j ]

具体分析思路如上,接下来给大家展示核心代码部分,分析部分没有看明白的地方,希望代码可以帮助理解。

核心代码如上,图片如果看不清楚,打开网页http://paste.ubuntu.com/25406352/查看核心代码。今天的分享就到这里,希望大家如果发现文章中写的不恰当的或者有错误的地方能够积极在留言区留言。

还没有关注我的朋友,可以识别下方二维码进行关注,虽然不能保证每日更新一篇,但每篇均属作者本人原创,质量可以保证。

时间: 2024-10-12 22:44:56

LCA问题的相关文章

HDU 6203 ping ping ping [LCA,贪心,DFS序,BIT(树状数组)]

题目链接:[http://acm.hdu.edu.cn/showproblem.php?pid=6203] 题意 :给出一棵树,如果(a,b)路径上有坏点,那么(a,b)之间不联通,给出一些不联通的点对,然后判断最少有多少个坏点. 题解 :求每个点对的LCA,然后根据LCA的深度排序.从LCA最深的点对开始,如果a或者b点已经有点被标记了,那么continue,否者标记(a,b)LCA的子树每个顶点加1. #include<Bits/stdc++.h> using namespace std;

SZOJ 167 Lca裸题

一道.......一道我改了一周的裸题 无根树建双向边 无根树建双向边 无根树建双向边 重要的事情说三遍(微笑) 还有要开longlong 还有双向边不是双倍边(微笑) 我真是,能把自己气吐血10次就不把自己气吐血9次 [问题描述] 已知一棵nn个点的树,点从1开始标号,树上每条边都有一个正整数边权. 有qq个询问,每个询问由type,u,vtype,u,v三个正整数构成. 当type=1type=1时,询问uu到vv路径上所有边权的二进制异或和. 当type=2type=2时,询问uu到vv路

【LCA/tarjan】POJ1470-Closest Common Ancestors

[题意] 给出一棵树和多组查询,求以每个节点为LCA的查询数有多少? [错误点] ①读入的时候,注意它的空格是随意的呀!一开始不知道怎么弄,后来看了DISCUSS区大神的话: 询问部分输入: scanf("%d",&m); for(int i=0;i<m;i++){ scanf(" (%d %d)",&a,&b); } 注意scanf(" 这里有一个空格 ②多组数据啊!注意这句话:The input file contents

习题:过路费(kruskal+并查集+LCA)

过路费  [问题描述]在某个遥远的国家里,有 n 个城市.编号为 1,2,3,…,n.这个国家的政府修 建了 m 条双向道路,每条道路连接着两个城市.政府规定从城市 S 到城市 T 需 要收取的过路费为所经过城市之间道路长度的最大值.如:A 到 B 长度为 2,B 到 C 长度为 3,那么开车从 A 经过 B 到 C 需要上交的过路费为 3. 佳佳是个做生意的人,需要经常开车从任意一个城市到另外一个城市,因此 他需要频繁地上交过路费,由于忙于做生意,所以他无时间来寻找交过路费最低 的行驶路线.然

hdu3078 建层次树+在线LCA算法+排序

题意:n个点,n-1条边构成无向树,每个节点有权,Q次询问,每次或问从a->b的最短路中,权第k大的值,/或者更新节点a的权, 思路:在线LCA,先dfs生成树0,标记出层数和fa[](每个节点的父亲节点).在对每次询问,走一遍一次公共祖先路上 的权,保持,快排.n*logn*q #include<iostream> //187MS #include<algorithm> #include<cstdio> #include<vector> using

HDU 6203 ping ping ping(dfs序+LCA+树状数组)

http://acm.hdu.edu.cn/showproblem.php?pid=6203 题意: n+1 个点 n 条边的树(点标号 0 ~ n),有若干个点无法通行,导致 p 组 U V 无法连通.问无法通行的点最少有多少个. 思路: 贪心思维,破坏两个点的LCA是最佳的.那么怎么判断现在在(u,v)之间的路径上有没有被破坏的点呢,如果没有的话那么此时就要破坏这个lca点.一开始我们要把询问按照u和v的lca深度从大到小排序,如果某个点需要被破坏,那么它的所有子节点都可以不再需要破坏别的点

【bzoj1146】[CTSC2008]网络管理Network 倍增LCA+dfs序+树状数组+主席树

题目描述 M公司是一个非常庞大的跨国公司,在许多国家都设有它的下属分支机构或部门.为了让分布在世界各地的N个部门之间协同工作,公司搭建了一个连接整个公司的通信网络.该网络的结构由N个路由器和N-1条高速光缆组成.每个部门都有一个专属的路由器,部门局域网内的所有机器都联向这个路由器,然后再通过这个通信子网与其他部门进行通信联络.该网络结构保证网络中的任意两个路由器之间都存在一条直接或间接路径以进行通信. 高速光缆的数据传输速度非常快,以至于利用光缆传输的延迟时间可以忽略.但是由于路由器老化,在这些

【最近公共祖先Tarjan】Tarjan求LCA练习

Tarjan求LCA 这是一篇非常好的讲解,靠这个文章搞懂的~ 1 void tarjan(int u) 2 { 3 vis[u]=1; 4 for(int i=0;i<edge[u].size();i++) 5 { 6 int v=edge[u][i]; 7 if(vis[v] == 0) 8 { 9 tarjan(v); 10 p[v]=u; 11 } 12 } 13 for(int i=0;i<qy[u].size();i++) 14 { 15 int v=qy[u][i].v,id=q

hdu3830(lca + 二分)

题目链接: http://acm.hdu.edu.cn/showproblem.php?pid=3830 题意: 有三个点 a, b, c, 对于其中任意一点 x 可以跨过一个点移动到另一个位置, 当且仅当移动前后的 x 与其所跨越的点的距离相等 .给出两组点, 问其能否相互到达, 若能并输出最少需要移动多少步 . 思路: http://www.cnblogs.com/scau20110726/archive/2013/06/14/3135024.html 代码: 1 #include <ios

HDU 6065 RXD, tree and sequence (LCA DP)

RXD, tree and sequence Time Limit: 6000/3000 MS (Java/Others)    Memory Limit: 524288/524288 K (Java/Others)Total Submission(s): 234    Accepted Submission(s): 82 Problem Description RXD has a rooted tree T with size n, the root ID is 1, with the dep