LCA之Tarjan算法解析

好久没动手做算法了,今天趁着有空,想学下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)的查询,也不会有答案输出。所以我们两个节点都需要记录。

好了,说的就这么多!




  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
LCA(最近公共祖先)是指在一棵树中,找到两个节点的最近的共同祖先节点。而Tarjan算法是一种用于求解强连通分量的算法,通常应用于有向图中。它基于深度优先搜索(DFS)的思想,通过遍历图中的节点来构建强连通分量。Tarjan算法也可以用于求解LCA问题,在有向无环图(DAG)中。 具体来说,在使用Tarjan算法求解LCA时,我们需要进行两次DFS遍历。首先,我们从根节点开始,遍历每个节点,并记录每个节点的深度(即从根节点到该节点的路径长度)。然后,我们再进行一次DFS遍历,但这次我们在遍历的过程中,同时进行LCA的查找。对于每个查询,我们将两个待查询节点放入一个查询列表中,并在遍历过程中记录每个节点的祖先节点。 在遍历的过程中,我们会遇到以下几种情况: 1. 如果当前节点已被访问过,说明已经找到了该节点的祖先节点,我们可以更新该节点及其所有后代节点的祖先节点。 2. 如果当前节点未被访问过,我们将其标记为已访问,并将其加入到查询列表中。 3. 如果当前节点有子节点,我们继续递归遍历子节点。 最终,对于每个查询,我们可以通过查询列表中的两个节点的最近公共祖先节点来求解LCA。 需要注意的是,Tarjan算法的时间复杂度为O(V+E),其中V为节点数,E为边数。因此,对于大规模的树结构,Tarjan算法是一种高效的求解LCA问题的方法。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值