LCA - Tarjan 模板 介绍与实现

普通的介绍

在我们小学二年级学习过的《算法导论》第三版 第337页上,有一个思考题(什!这么重要的内容只是思考题吗!)
介绍的就是Tarjan 的脱机最小公共祖先
(Tarjan’s Offline Least-Common-Ancestors Problem)

什么是LCA

LCA,就是 Least-Common-Ancestors ,指在一棵树中,两个节点的最近的公共祖先。
在这里插入图片描述
在这棵以E为根的树中,A节点和B节点的最近公共祖先为节点C。而节点D和节点E虽然也为A与B节点的祖先,但是不是最近的祖先。

为什么我需要Tarjan算法求LCA?

在一个N个节点的树中
若我们使用最暴力的普通算法
(即算出两个点的深度,深度大的那个点往上访问,直到两个点汇合)
易得Q次查询的时间复杂度为O(NQ)

但是如果我们使用Tarjan算法
Q次查询的时间复杂度为O(N+Q)!这是个不小的改进!
若执行过一次该算法,便可以求出所有点对之前的LCA
(但是需要O(N2)空间来存储,或者我们使用vector或者数组存储就可以O(N)存储!)

原理ADT

Tarjan 的 LCA的ADT如下:

LCA(node){
    anc[find_fa(node)] = node;
    for(auto &it : G[node]){
        LCA(it)
        add(node,it)
        anc[find_fa(node)] = node
    }
    flag[node] = 1
    for(auto &it : Q[node])
        if(flag[it])
           print ("node 和 it 的 LCA 为:" anc[find_fa(it)])
}

读者应该发现了,该核心ADT主要用到了并查集(Union Find)的内容!
(并查集代码建议使用启发式策略,rank比较与路径压缩,可以使得并查集部分的时间复杂度更加优秀)

  1. 首先代码第二行,把这个节点的代表的anc祖先设置为他本身
  2. 遍历他的每一个孩子,递归LCA(每个node的孩子)
  3. 并查集,把node和他的孩子并起来,把这个节点的代表的anc祖先再次设置为他本身
  4. 孩子遍历完后,该节点的flag设置为1(表示该节点的所有孩子都遍历完了,即该节点已经访问完毕)
  5. 遍历所有查询,如果查询里面需要询问node 和 it 的祖先 , 并且it 节点也访问过的话,输出 it所在集合的anc祖先

容易证明,对于每对不同的点对,if(flag[it])只会运行一次。
理性的证明这里没有(笔者太菜了!)
感性的证明:并查集后,所有直接儿子的LCA都设置成为了root,因此询问结果总是正确的(?)

模板题目

最近公共祖先(LCA) - 洛谷

AC模板代码

照着算法导论的十二行ADT,手写并查集,vector搞一搞,AC!

///时间复杂度 O(N+Q)
#include <bits/stdc++.h>
#define show(x) std::cerr << #x << "=" << x << std::endl
#define IOS ios::sync_with_stdio(false);cin.tie(NULL);cout.tie(NULL);
typedef long long ll;
using namespace std;
const int MAX = 5e5+50;
const int MOD = 1e9+7;
const int INF = 0x3f3f3f3f;
vector<int>G[MAX];
vector<pair<int,int> >Q[MAX];
int anc[MAX],fa[MAX],in[MAX],flag[MAX],rk[MAX];
int root;
int ans[MAX];
void init(int n,int m){
    memset(anc,0,sizeof(anc));
    memset(flag,0,sizeof(flag));
    memset(rk,0,sizeof(rk));
    memset(in,0,sizeof(in));
    for(int i=0;i<=n;++i){
        fa[i] = i;
        G[i].clear();
        Q[i].clear();
    }
    for(int i=1;i<n;++i){
        int x,y;
        cin >> x >> y;
        G[x].push_back(y);
        G[y].push_back(x);
    }
    for(int i=1;i<=m;++i){
        int ta,tb;cin >> ta >> tb;
        Q[ta].push_back(make_pair(tb,i));
        Q[tb].push_back(make_pair(ta,i));
    }
}
int find_fa(int x){
    if(x==fa[x])return x;
    return fa[x] = find_fa(fa[x]);
}
void add(int x,int y){
    int fx = find_fa(x);
    int fy = find_fa(y);
    if(rk[fx] > rk[fy])fa[fy] = fx;
    else fa[fx] = fy;
    if(rk[fx] == rk[fy])rk[fy]++;
}
void LCA(int node,int fa){
    for(auto &it : G[node]){
        if(it == fa)continue;
        LCA(it,node);
        add(node,it);
        anc[find_fa(node)] = node;
    }
    flag[node] = 1;
    for(auto &it : Q[node]){
        if(flag[it.first]){
            ans[it.second] = anc[find_fa(it.first)];
        }
    }
}
void ANS(int m){
    for(int i=1;i<=m;++i){
        cout << ans[i];
        if(i!=m)cout << endl;
    }
}
int main()
{
    IOS;
    int n,m,s;
    cin >> n >> m >> s;
    init(n,m);
    LCA(s,s);
    ANS(m);
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值