离线Tarjan算法-最近公共祖先问题

转载自Tarjan算法

LCA问题(Least 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
	*/
}

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏灯塔大数据

每周学点大数据 | No.25二叉搜索树回顾(二)

No.25期 二叉搜索树回顾(二) Mr. 王:既然提到了有序的状态,那么我们就来谈谈有根树的遍历问题。 小可:我知道,遍历就是访问一个数据集合中的所...

35560
来自专栏数据结构与算法

树链剖分详解

前言 树链剖分是什么? 树链剖分,说白了就是一种让你代码不得不强行增加1k的数据结构-dms 个人理解:+1:joy: 有什么用? 证明出题人非常毒瘤 ...

33670
来自专栏一名叫大蕉的程序员

大数据计数原理1+0=1这你都不会算(四)No.52

这是本坑的第四篇,之前已经说了关于 HashSet 、BitMap 、Bloom Filter 布隆过滤器了,本篇主要讲B-树。要是还不知道前面讲了啥的,可以点...

21770
来自专栏Java后端技术栈

为什么MySQL数据库索引选择使用B+树?

在进一步分析为什么MySQL数据库索引选择使用B+树之前,我相信很多小伙伴对数据结构中的树还是有些许模糊的,因此我们由浅入深一步步探讨树的演进过程,在一步步引出...

45810
来自专栏项勇

笔记26 | 总结Android的获取系统时间的几种方法

20350
来自专栏猿人谷

双向链表

双向链表       在线性链式存储结构的结点中只有一个指示直接后继的指针域,由此,从某个结点出发只能顺指针往后寻查其他结点。若要寻查结点的直接前趋,则需从表...

28150
来自专栏Java爬坑系列

【Java入门提高篇】Day31 Java容器类详解(十三)TreeSet详解

  TreeSet是Set家族中的又一名懒将,跟其他两位一样,与对应的Map关系密不可分

9830
来自专栏尾尾部落

[剑指offer] 重建二叉树

输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6...

16310
来自专栏趣学算法

数据结构 第13讲 三元组 (F、C、L/R) 序列创建二叉树

/* 输入三元组 (F、C、L/R) 序列输入一棵二叉树的诸边(其中 F 表示双亲结点的标识,C 表示孩子结点标识,L/R...

32530
来自专栏数据结构与算法

Day2平衡树笔记

线段树不支持的操作:删除,插入 ---- 常见的平衡树 treap 慢||好写 sbt(大小平衡的树) 非常快 比较好写 ||功能不全 rbt 红黑树 特...

32760

扫码关注云+社区

领取腾讯云代金券