(转载)LCA问题的Tarjan算法
转载自:Click Here
LCA问题(Lowest Common Ancestors,最近公共祖先问题),是指给定一棵有根树T,给出若干个查询LCA(u, v)(通常查询数量较大),每次求树T中两个顶点u和v的最近公共祖先,即找一个节点,同时是u和v的祖先,并且深度尽可能大(尽可能远离树根)。
LCA问题有很多解法:线段树、Tarjan算法、跳表、RMQ与LCA互相转化等。本文主要讲解Tarjan算法的原理及详细实现。
一 LCA问题
LCA问题的一般形式:给定一棵有根树,给出若干个查询,每个查询要求指定节点u和v的最近公共祖先。
LCA问题有两类解决思路:
- 在线算法,每次读入一个查询,处理这个查询,给出答案。
- 离线算法,一次性读入所有查询,统一进行处理,给出所有答案。
一个LCA的例子如下。比如节点1和6的LCA为0。

二 算法思路
Tarjan算法是离线算法,基于后序DFS(深度优先搜索)和并查集。如果不熟悉并查集,可以查看并查集及其在最小生成树中的应用。
算法从根节点root开始搜索,每次递归搜索所有的子树,然后处理跟当前根节点相关的所有查询。
算法用集合表示一类节点,这些节点跟集合外的点的LCA都一样,并把这个LCA设为这个集合的祖先。当搜索到节点x时,创建一个由x本身组成的集合,这个集合的祖先为x自己。然后递归搜索x的所有儿子节点。当一个子节点搜索完毕时,把子节点的集合与x节点的集合合并,并把合并后的集合的祖先设为x。因为这棵子树内的查询已经处理完,x的其他子树节点跟这棵子树节点的LCA都是一样的,都为当前根节点x。所有子树处理完毕之后,处理当前根节点x相关的查询。遍历x的所有查询,如果查询的另一个节点v已经访问过了,那么x和v的LCA即为v所在集合的祖先。
其中关于集合的操作都是使用并查集高效完成。
算法的复杂度为,O(n)搜索所有节点,搜索每个节点时会遍历这个节点相关的所有查询。如果总的查询个数为m,则总的复杂度为O(n+m)。
比如上面的例子中,前面处理的节点的顺序为4->7->5->1->0->…。
当访问完4之后,集合{4}跟集合{1}合并,得到{1,4},并且集合祖先为1。然后访问7。如果(7,4)是一个查询,由于4已访问过,于是LCA(7,4)为4所在集合{1,4}的祖先,即1。7访问完之后,把{7}跟{5}合并,得到{5,7},祖先为5。然后访问5。如果(5,7)是一个查询,由于7已访问过,于是LCA(5,7)为7所在集合{5,7}的祖先,即5。如果(5,4)也是一个查询,由于4已访问过,则LCA(5,4)为4所在集合{1,4}的祖先,即1。5访问完毕之后,把{5,7}跟{1,4}合并,得到{1,4,5,7},并且祖先为1。然后访问1。如果有(1,4)查询,则LCA(1,4)为4所在集合{1,4}的祖先,为1。1访问完之后,把{1,4,5,7}跟{0}合并,得到{0,1,4,5,7},祖先为0。然后剩下的2后面的节点处理类似。
三 算法实现
接下来提供一个完整算法实现。
使用邻接表方法存储一棵有根树。并通过记录节点入度的方法找出有根树的根,方便后续处理。
const int mx = 10000; //最大顶点数
int n, root; //实际顶点个数,树根节点
int indeg[mx]; //顶点入度,用来判断树根
vector<int> tree[mx]; //树的邻接表(不一定是二叉树)
void inputTree() //输入树
{
scanf("%d", &n); //树的顶点数
for (int i = 0; i < n; i++) //初始化树,顶点编号从0开始
tree[i].clear(), indeg[i] = 0;
for (int i = 1; i < n; i++) //输入n-1条树边
{
int x, y; scanf("%d%d", &x, &y); //x->y有一条边
tree[x].push_back(y); indeg[y]++;//加入邻接表,y入度加一
}
for (int i = 0; i < n; i++) //寻找树根,入度为0的顶点
if (indeg[i] == 0) { root = i; break; }
}
使用vector数组query存储所有的查询。跟x相关的所有查询(x,y)都会放在query[x]的数组中,方便查找。
vector<int> query[mx]; //所有查询的内容
void inputQuires() //输入查询
{
for (int i = 0; i < n; i++) //清空上次查询
query[i].clear();
int m; scanf("%d", &m); //查询个数
while (m--)
{
int u, v; scanf("%d%d", &u, &v); //查询u和v的LCA
query[u].push_back(v); query[v].push_back(u);
}
}
然后是并查集的相关数据和操作。
int father[mx], rnk[mx]; //节点的父亲、秩
void makeSet() //初始化并查集
{
for (int i = 0; i < n; i++) father[i] = i, rnk[i] = 0;
}
int findSet(int x) //查找
{
if (x != father[x]) father[x] = findSet(father[x]);
return father[x];
}
void unionSet(int x, int y) //合并
{
x = findSet(x), y = findSet(y);
if (x == y) return;
if (rnk[x] > rnk[y]) father[y] = x;
else father[x] = y, rnk[y] += rnk[x] == rnk[y];
}
再就是Tarjan算法的核心代码。
在调用Tarjan之前已经初始化并查集给每个节点创建了一个集合,并且把集合的祖先赋值为自己了,因而这里不用给根节点x单独创建。
int ancestor[mx]; //已访问节点集合的祖先
bool vs[mx]; //访问标志
void Tarjan(int x) //Tarjan算法求解LCA
{
for (int i = 0; i < tree[x].size(); i++)
{
Tarjan(tree[x][i]); //访问子树
unionSet(x, tree[x][i]); //将子树节点与根节点x的集合合并
ancestor[findSet(x)] = x;//合并后的集合的祖先为x
}
vs[x] = 1; //标记为已访问
for (int i = 0; i < query[x].size(); i++) //与根节点x有关的查询
if (vs[query[x][i]]) //如果查询的另一个节点已访问,则输出结果
printf("%d和%d的最近公共祖先为:%d\n", x,
query[x][i], ancestor[findSet(query[x][i])]);
}
下面是主程序,再加一个样例输入输出作为测试。
int main()
{
inputTree(); //输入树
inputQuires();//输入查询
makeSet();
for (int i = 0; i < n; i++) ancestor[i] = i;
memset(vs, 0, sizeof(vs)); //初始化为未访问
Tarjan(root);
/*前面例子相关的一个输入输出如下:
8
0 1 0 2 0 3 1 4 1 5 5 7 3 6
7
1 4 4 5 4 7 5 7 0 5 4 3 1 6
7和4的最近公共祖先为:1
5和4的最近公共祖先为:1
5和7的最近公共祖先为:5
1和4的最近公共祖先为:1
6和1的最近公共祖先为:0
3和4的最近公共祖先为:0
0和5的最近公共祖先为:0
*/
}
(转载)LCA问题的Tarjan算法的更多相关文章
- LCA 离线的Tarjan算法 poj1330 hdu2586
LCA问题有好几种做法,用到(tarjan)图拉算法的就有3种.具体可以看邝斌的博客.http://www.cnblogs.com/kuangbin/category/415390.html 几天的学 ...
- POJ 1470 Closest Common Ancestors (LCA,离线Tarjan算法)
Closest Common Ancestors Time Limit: 2000MS Memory Limit: 10000K Total Submissions: 13372 Accept ...
- Tarjan算法离线 求 LCA(最近公共祖先)
本文是网络资料整理或部分转载或部分原创,参考文章如下: https://www.cnblogs.com/JVxie/p/4854719.html http://blog.csdn.net/ywcpig ...
- 【POJ 1330 Nearest Common Ancestors】LCA问题 Tarjan算法
题目链接:http://poj.org/problem?id=1330 题意:给定一个n个节点的有根树,以及树中的两个节点u,v,求u,v的最近公共祖先. 数据范围:n [2, 10000] 思路:从 ...
- POJ 1330 Nearest Common Ancestors(LCA Tarjan算法)
题目链接:http://poj.org/problem?id=1330 题意:给定一个n个节点的有根树,以及树中的两个节点u,v,求u,v的最近公共祖先. 数据范围:n [2, 10000] 思路:从 ...
- Tarjan算法应用 (割点/桥/缩点/强连通分量/双连通分量/LCA(最近公共祖先)问题)(转载)
Tarjan算法应用 (割点/桥/缩点/强连通分量/双连通分量/LCA(最近公共祖先)问题)(转载) 转载自:http://hi.baidu.com/lydrainbowcat/blog/item/2 ...
- 最近公共祖先LCA(Tarjan算法)的思考和算法实现——转载自Vendetta Blogs
LCA 最近公共祖先 Tarjan(离线)算法的基本思路及其算法实现 小广告:METO CODE 安溪一中信息学在线评测系统(OJ) //由于这是第一篇博客..有点瑕疵...比如我把false写成了f ...
- LCA:Tarjan算法实现
本博文转自http://www.cnblogs.com/JVxie/p/4854719.html,转载请注明出处 首先是最近公共祖先的概念(什么是最近公共祖先?): 在一棵没有环的树上,每个节点肯定有 ...
- 求LCA最近公共祖先的离线Tarjan算法_C++
这个Tarjan算法是求LCA的算法,不是那个强连通图的 它是 离线 算法,时间复杂度是 O(m+n),m 是询问数,n 是节点数 它的优点是比在线算法好写很多 不过有些题目是强制在线的,此类离线算法 ...
随机推荐
- Block Chain, a protocol view
我做了个区块链的文档,给自己扫盲用的,有兴趣的可以看下,主要是自己画示意图比较好理解,示意图之后的专题部分,内容直接取自参考链接.网上的资料都是谈区块链有什么性质.有什么能力.有什么应用之类的,我主要 ...
- swift导航栏导航按钮添加多个按钮事件
//导航左边返回按钮 let button1 = UIButton(frame:CGRectMake(0, 0, 18, 18)) button1.setImage(Constant.Image.Na ...
- Spring MVC 前后端 Json 方式交互和处理
众所周知,在mvc中,数据是在各个层次之间进行流转是一个不争的事实. 而这种流转,也就会面临一些困境,这些困境,是由于数据在不同世界中的表现形式不同而造成的. 数据在页面上是一个扁平的,不带数据类 ...
- DTCMS插件的制作实例电子资源管理(一)插件目录结构
总目录 插件目录结构(一) Admin后台页面编写(二) 前台模板页编写(三) URL重写(四) 本实例旨在以一个实际的项目中的例子来介绍如何在dtcms中制作插件,本系列文章非入门教程,部分 ...
- 告别编译运行 ---- Android Studio 2.0 Preview发布Instant Run功能
以往的Android开发有一个头疼的且拖慢速度的问题,就是你每改一行代码要想看到结果必须要编译运行到手机或者模拟器上,而且需要从头(可能是登录界面)一直点击到你修改的界面为止.开发一个完整的Andro ...
- 如何在Vue2中实现组件props双向绑定
Vue学习笔记-3 前言 Vue 2.x相比较Vue 1.x而言,升级变化除了实现了Virtual-Dom以外,给使用者最大不适就是移除的组件的props的双向绑定功能. 以往在Vue1.x中利用pr ...
- HashSet<T>类 用法
HashSet<T>类主要是设计用来做高性能集运算的,例如对两个集合求交集.并集.差集等.集合中包含一组不重复出现且无特性顺序的元素 改变集的值的方法: HashSet<T>的 ...
- [BZOJ3211]花神游历各国(线段树+区间开根)
题目:http://www.lydsy.com:808/JudgeOnline/problem.php?id=3211 分析: 区间开根是没法区间合并的. 但是注意到10^9开根开个5次就变成1了…… ...
- TAR命令详解
上图,VPN截图,画蛇添足! 在Linux中,压缩与解压用得最多的tar.tar命令确实很厉害. tar -c: 建立压缩档案-x:解压-t:查看内容-r:向压缩归档文件末尾追加文件-u:更新原压缩包 ...
- Android 之px于dp在Java代码中的转换
现在由于用到了,使用代码进行动态布局,所以需要进行px于dp之间的转换. 现将其封装为方法,以便于调用. public int DpToPx(Context context,float dp){ fl ...