LCA(离线Tarjan算法,在线倍增法)详解

LCA(离线Tarjan算法,在线倍增法)详解


首先我们看一道洛谷上的板子题:

P3379 【模板】最近公共祖先(LCA)

时空限制:1000ms,128M

【题目描述】

如题,给定一棵有根多叉树,请求出指定两个点直接最近的公共祖先。

【输入格式】

第一行包含三个正整数N、M、S,分别表示树的结点个数、询问的个数和树根结点的序号。
接下来N-1行每行包含两个正整数x、y,表示x结点和y结点之间有一条直接连接的边(数据保证可以构成树)。
接下来M行每行包含两个正整数a、b,表示询问a结点和b结点的最近公共祖先。

【输出格式】

输出包含M行,每行包含一个正整数,依次为每一个询问的结果。

【输入样例】

5 5 4
3 1
2 4
5 1
1 4
2 4
3 2
3 5
1 2
4 5

【输出样例】

4
4
1
4
4

【数据范围】

对于30%的数据:N<=10,M<=10;
对于70%的数据:N<=10000,M<=10000;
对于100%的数据:N<=500000,M<=500000;

这题读完就有一个想法,就是建树,然后一直往往父节点身上找。
但是时间复杂度是O(N*M)的,也就是O(25*10^10)不要想了,70分都难拿到手。
这时,就要用到LCA了。

下面介绍两种方法:

离线Tarjan算法

操作步骤如图:

假设是这样一棵树。

这里写图片描述

第一次遍历到底(红色表示被遍历的点,图片有点难看,见谅)。

这里写图片描述

如果询问6和4,它们的最近公共祖先是4。

第二次遍历到底(蓝色表示遍历完的点)。

这里写图片描述

当此时询问7和6时,它们的最近公共祖先find(6)(find(x)在后面会解释,因为这条线路上都是7的祖先,所以不用find(7)),也就是4。

第三次遍历到底。

这里写图片描述

如果求8和7的最近公共祖先,那么就是find(7),就是2。

第四次遍历到底。

这里写图片描述

如果求3和4的最近公共祖先,那么就是find(4),就是1。

通过以上图片,我们知道find(x)表示离自己最近的被遍历过的点(跳过被遍历完的点)。还有前提,两个点都被遍历过。但是如果这样,还是很慢啊。
其实可以用并查集来写find(x),这样不是快多了?O(1)出答案,但是遍历的过程是O(N)的效率,因为有M次询问,所以总时间效率是O(N+M)。
但是答案是在过程中得出的,是无序的(所以我们需要使它有序,在代码中详解)。

#include<cstdio>
#define MAXN 500005
using namespace std;
int n,q,m,fa[MAXN];
int lnk[2][MAXN],nxt[2][2*MAXN],son[2][2*MAXN],tot[2],ans[MAXN];//ans[]存答案 
bool vis[MAXN];
void add(int a,int x,int y){son[a][++tot[a]]=y;nxt[a][tot[a]]=lnk[a][x];lnk[a][x]=tot[a];}//邻接表 
int read(){//读入 
    int ret=0;bool f=1;char ch=getchar();
    for(;ch<'0'||'9'<ch;ch=getchar()) f^=!(ch^'-');
    for(;ch>='0'&&ch<='9';ch=getchar()) ret=(ret<<3)+(ret<<1)+ch-48;
    return f?ret:-ret;
}
int get(int x){return fa[x]==x?x:fa[x]=get(fa[x]);}//并查集 
int mer(int x,int y){//合并 
    int fx=get(x),fy=get(y);
    if(fx!=fy) fa[fy]=fx;
}
int DFS(int x){//LCA用DFS实现 
    vis[x]=1;
    for(int j=lnk[1][x];j;j=nxt[1][j])//枚举问题 
    if(vis[son[1][j]]) ans[(j+1)/2]=get(son[1][j]);//因为是邻接表,所以j就是读入的序号,但是因为存入了询问(a,b)(b,a)两个,所以要(j+1)/2(奇偶性) 

    for(int j=lnk[0][x];j;j=nxt[0][j])
    if(!vis[son[0][j]]) DFS(son[0][j]),mer(x,son[0][j]);//接下去遍历它的儿子节点 ,遍历完后合并。 
}
int main(){
    freopen("LCA.in","r",stdin);
    freopen("LCA.out","w",stdout);
    n=read();q=read();m=read();//q是寻问数 ,m是根节点。 
    for(int i=1;i<=n;i++) fa[i]=i;
    for(int i=1;i<n;i++){
        int x=read(),y=read();
        add(0,x,y);add(0,y,x);
    }
    for(int i=1;i<=q;i++){
        int a=read(),b=read();
        add(1,a,b);add(1,b,a);//因为有可能出现a被遍历到了,而b没有,然后就无法输出答案。 
    }
    DFS(m);
    for(int i=1;i<=q;i++) printf("%d\n",ans[i]);
    return 0;
}

在线倍增法

这个就有点像RMQ了,其实就是树上RMQ罢了,f[i][j]表示从i开始,向上2j的节点编号,我们设x=f[i][j1]那么,f[i][j]=f[x][j1]
取答案的时候,先将两个数提到同一高的,然后,一同往上跳,就能做到O(n*log(n))的处理,O(log(n))的取答案。

代码如下:

#include<cstdio>
#include<iostream>
#define MAXN 500005
using namespace std;
int n,q,g,fa[MAXN];
int lnk[MAXN],nxt[2*MAXN],son[2*MAXN],tot,f[MAXN][30],dep[MAXN];//dep[i]是i这个节点的深度 
bool vis[MAXN];
void add(int x,int y){son[++tot]=y;nxt[tot]=lnk[x];lnk[x]=tot;}//邻接表 
int read(){//读入 
    int ret=0;bool f=1;char ch=getchar();
    for(;ch<'0'||'9'<ch;ch=getchar()) f^=!(ch^'-');
    for(;ch>='0'&&ch<='9';ch=getchar()) ret=(ret<<3)+(ret<<1)+ch-48;
    return f?ret:-ret;
}
void DFS(int x,int deep){//无根树变成有根树 
    dep[x]=deep;
    for(int j=lnk[x];j;j=nxt[j])
    if(!vis[son[j]]) vis[son[j]]=1,fa[son[j]]=x,DFS(son[j],deep+1);
}
void change(){
    for(int i=1;i<=n;i++){
        f[i][0]=fa[i];
        for(int j=1;(1<<j)<n;j++) f[i][j]=-1;//赋初值,无祖先为-1 
    }
    for(int j=1;(1<<j)<n;j++)
    for(int i=1,x;i<=n;i++) if(f[i][j-1]^-1) x=f[i][j-1],f[i][j]=f[x][j-1];//^可以理解为!=,但是^更快 
}
int get(int p,int q){
    int log;
    if(dep[p]<dep[q]) swap(p,q);//我们设p的深度大 
    for(log=1;(1<<log)<=dep[p];log++);log--; 
    for(int i=log;i+1;i--)//将p往上提,"i+1"可以认为是i>=0 
    if(dep[p]-(1<<i)>=dep[q]) p=f[p][i];
    if(p==q) return p;//或q 
    for(int i=log;i+1;i--)//同时往上跳 
    if(f[p][i]^-1&&f[p][i]^f[q][i]) p=f[p][i],q=f[q][i];
    return fa[p];//或fa[q];
}
int main(){
    n=read(),q=read(),g=read();
    for(int i=1;i<n;i++){
        int x=read(),y=read();
        add(x,y);add(y,x);
    }
    fa[g]=g;vis[g]=1;//原点打掉 
    DFS(g,0);change();
    for(int i=1;i<=n;i++){
        int x=read(),y=read();
        printf("%d\n",get(x,y));
    }
    return 0;
}

如果还不懂,可以在评论里提问,我看到会解答的哦

转载于:https://www.cnblogs.com/XSamsara/p/9059479.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值