最近公共祖先(LCA) Tarjan算法

这里的动画演示可以更好地理解这一算法

Tarjan算法是由Robert Tarjan在1979年发现的一种高效的离线算法,也就是说,它要首先读入所有的询问(求一次LCA叫做一次询问),然后并不一定按照原来的顺序处理这些询问,LCA Tarjan算法的用途是处理大量请

LCA Tarjan基本框架:

  • 先用随便一种数据结构(链表就行),把关于某个点的所有询问标在节点上,保证遍历到一个点,能得到所有有关这个节点LCA 查询
  • 建立并查集.注意:这个并查集只可以把叶子节点并到根节点,即getf(x)得到的总是x的祖先
  • 深度优先遍历整棵树,用一个Visited数组标记遍历过的节点,每遍历到一个节点将Visite[i]设成True 处理关于这个节点(不妨设为A)的询问,若另一节点(设为B)的Visited[B]==True,则回应这个询问,这个询问的结果就是getf(B). 否则什么都不做
  • 当A所有子树都已经遍历过之后,将这个节点用并查集并到他的父节点(其实这一步应该说当叶子节点回溯回来之后将叶子节点并到自己,并DFS另一子树)
  • 当一颗子树遍历完时,这棵子树的内部查询(即LCA在这棵子树内部)都已经处理了
如果采用不相交集森林的方法来实现并查集并采用路径压缩来优化,这样Find操作的时间复杂度可以认为是常数级别的。
所以Tarjan算法的时间复杂度就是O(N + Q*a(N)),a(N)在可以计算的范围内是一个小于4的常数,空间复杂度为O(N),其中N表示问题规模,Q表示询问次数。

#pragma warning (disable:4786)  
#include<iostream>
#include<stdio.h>
#include<vector> 
using namespace std;
const int MAX=17;
int father[MAX];         //并查集父亲节点         
int r[MAX];              //秩,union操作时,将秩小的集合并入较大的
int indegree[MAX];       //入度,用于寻找根节点
int visit[MAX];          //记录树中的节点是否被访问过,是为1,否为0
vector <int> tree[MAX],Qes[MAX];   //分别用来存储孩子节点和查询对象  
int ancestor[MAX];                    //每个节点的祖先

//初始化
void init(int n){
	for(int i=1;i<=n;i++){
		r[i]=1;
		father[i]=i;
		indegree[i]=0;
		visit[i]=0;
		ancestor[i]=0;
		tree[i].clear();
		Qes[i].clear();
	}
}
//并查集寻找祖先操作,路径压缩
int findSet(int n){
	if(father[n]==n)
		return n;
	else return father[n]=findSet(father[n]);
}

//并查集合并操作,秩(树的高度)小的集合合并到秩大的
void Union(int x, int y) {
	int f1 = findSet(x);
	int f2 = findSet(y);
	
	if (r[f1] <= r[f2]) {
		father[f1] = f2;
		if (r[f1] == r[f2]) {
			r[f2] ++; 	
		}	
	} else {
		father[f2] = f1;
	}
};
//寻找最近祖先
void LCA(int u){
	ancestor[u]=u;             //节点的祖先先设为自己
	int size=tree[u].size();   //子树个数
	for(int i=0;i<size;i++){   //先将子树遍历,同时将其LCA查询处理完毕
		LCA(tree[u][i]);
		Union(u,tree[u][i]);
		ancestor[findSet(u)]=u;
	}
	visit[u]=1;               //标记为已访问过
	size=Qes[u].size();

	//处理有关此节点的了LCA查询
	for(i=0;i<size;i++){
		if(visit[Qes[u][i]]==1){
			cout<<ancestor[findSet(Qes[u][i])]<<endl;    //如果另一个节点是已访问过的节点,
			                                          //那么根据深度优先查找的性质直接得出结果
			continue;
		}
	}
}
int main()  
{  
	int n = 16;  
	init(n);			//数的总节点数
	int s,t;  

	//先构造树
	tree[8].push_back(5);indegree[5]++;
	tree[8].push_back(4);indegree[4]++;
	tree[8].push_back(1);indegree[1]++;					//对节点ID为8的节点添加3个子节点,相应的子节点增加入度

	tree[5].push_back(9);indegree[9]++;

	tree[4].push_back(6);indegree[6]++;
	tree[4].push_back(10);indegree[10]++;

	tree[1].push_back(14);indegree[14]++;
	tree[1].push_back(13);indegree[13]++;

	tree[6].push_back(15);indegree[15]++;
	tree[6].push_back(7);indegree[7]++;

	tree[10].push_back(11);indegree[11]++;
	tree[10].push_back(16);indegree[16]++;
	tree[10].push_back(2);indegree[2]++;

	tree[16].push_back(3);indegree[3]++;
	tree[16].push_back(12);indegree[12]++;

	//输入查询
	cin>>s>>t;  
	//相当于询问两次,如果t在s的左边,那么在遍历完s时将无法得出结果  
	Qes[s].push_back(t);
	Qes[t].push_back(s);

	for(int i=1;i<=n;i++)  
	{  
		//寻找根节点  
		if(indegree[i]==0)			//根节点的入度为0
		{  
			LCA(i);  
			break;  
		}  
	}  
	return 0;  
} 


除了上述的并查集+DFS的算法外,还可以把问题变成:无向图,求最短路中深度最小的节点


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值