好久没动手做算法了,今天趁着有空,想学下LCA的算法,于是看到了Tarjan算法。算法看着不难,但看了好多大神的博客,我只能大致理解他们的思路,始终不知道他们代码为何要那么写,再经过多番挣扎后,现在我终于弄清楚了,所以也和大家分享一下。
首先,我们要明白什么是最近公共祖先。
如图,这是poj的1330题,所谓公共祖先,即是两个节点共同的父节点,不过要注意,自己本身也算自己的父节点,而最近公共祖先,则是说该父节点距离两个子节点的距离最近,也就相当于层数越深。如果还有不了解的可以去看看poj1330,我们也会以那道题为例进行解析。
好了,现在正式讲解Tarjan算法
首先,我们要知道,假设f(x)的意思是指x的直接上层父节点,例如对上图,有f(6)=4,f(3)=16等。
那么,如果求u和v的最近公共祖先,我们能够知道,这个最进公共最先可能所处的情况。
(1)是u本身,例如图中的6和15;
(2) 是u的直接上层父节点,例如10和15;相当于f(x);
(3)类比(2),这样递归下去,可能是f(f(x)、f(f(f(x)))、.......
好了,知道这么多我们就可以很好的理解该算法了。
Tarjan算法是利用DFS和并查集的思想来做的,对于DFS,他首先访问其子节点,然后再访问父节点。利用这个特点,我们可以知道,处于同一棵子树的两个节点会先于父节点被访问到,因此,如果我们知道用来查找最近公共祖先的两个节点已经全被访问到了是不是说明它的最近公共祖先就是这棵子树的根呢?当然我们还需要用并查集把具有相同祖先的节点合并到一个集合中,这样当一个子树合并到一棵大树中去时,只需要执行一次合并操作,子树的所有节点祖先都得到更新。
于是,我们的Tarjan算法步骤也出来了
1.在并查集中建立一个仅有u的集合,其祖先为u;
2.对u的每个孩子
(1)tarjan之
(2)将其合并到u中去;
(3)将合并后的集合祖先置为u;
3.标记u已经访问。
4.处理关于u的查询,若查询(u,v)中的v已遍历过,则说明v的祖先即为u和v的最近公共祖先。
我们依然以POJ1330为例
以下是代码
#include <iostream>
#include <vector>
#include <cstring>
using namespace std;
const int MAXN=10001;
vector<int>Tree[MAXN];//根据题目给的数据建树
vector<int>Query[MAXN];//记录查询数据
int visit[MAXN];//记录节点是否已经被访问
int ancestor[MAXN];//记录当前分支的祖先
int degree[MAXN];//根据题意,用来记录祖先的数量,为0则是整棵树的根
int father[MAXN];//并查集合并后的前节点
int r[MAXN];//当前集合的秩
void init(int n){
for (int i=1; i<=n; i++) {
Tree[i].clear();
Query[i].clear();
visit[i]=0;
ancestor[i]=-1;
degree[i]=0;
father[i]=i;
r[i]=1;
}
}
int find(int x)
{
if(father[x]==x)
return x;
father[x]=find(father[x]);
return father[x];
}
void union_set(int x,int y)
{
int dx=father[x];
int dy=father[y];
if (dx==dy) {
return;
}
if (r[dx]>=r[dy]) {
r[dx]+=r[dy];
father[dy]=dx;
}
else{
r[dy]+=r[dx];
father[dx]=dy;
}
}
void tarjan(int u)
{
ancestor[u]=u;
int size=Tree[u].size();
for (int i=0; i<size; i++) {
tarjan(Tree[u][i]);
union_set(u, Tree[u][i]);
ancestor[find(u)]=u;
}
visit[u]=1;
size=Query[u].size();
for (int i=0; i<size; i++) {
if (visit[Query[u][i]]==1) {
cout<<ancestor[find(Query[u][i])]<<endl;
return;
}
}
}
int main(int argc, const char * argv[]) {
int t,n,a,b;
cin>>t;
while (t--) {
cin>>n;
init(n);
for (int i=1; i<n; i++) {
cin>>a>>b;
Tree[a].push_back(b);
degree[b]++;
}
cin>>a>>b;
Query[a].push_back(b);
Query[b].push_back(a);
for (int i=1; i<=n; i++) {
if (degree[i]==0) {
tarjan(i);
break;
}
}
}
return 0;
}
在这里提醒下大家注意看下题目,不然不好理解。
对于代码,这里指出几点:
1.degree的作用:因为题目给出的是父子关系,我们不能直接得到谁是整棵树的根,所以利用degree数组来记录入度数,入度数为0则为根。
2.题目中father和ancestor的作用,这里之所以用了father是为了更好的和并查集同步,与题目中的公共祖先分开,当然大家可以精简一点
3.为什么输入一个查询,要两个节点都记录?这是因为可能会遇到这种情况:u和v为需要查询的节点,题目给出的顺序是LCA(u,v),假设已经遍历了u,但没有遍历v,那此时不会有输出,而当再遍历到v时,此时由于不存在LCA(v,u)的查询,也不会有答案输出。所以我们两个节点都需要记录。
好了,说的就这么多!