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表示询问次数。
所以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的算法外,还可以把问题变成:无向图,求最短路中深度最小的节点