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

· · 题解

  1. 判断剩余要跳的层数的二进制的第 i 位是否是 1(实际上就是在判断往上跳 2^i 层后有没有超过另一个点), 如果是 1, 转至第 2 步, 否则, 转至第 3 步。
  2. 向上跳 2^i 层, 并转至第 3 步。

在两点同时跳时, 也是这样一个过程。
代码:

for(int i=1;i<=m;i++){
        u=read();
        v=read();
        if(dep[u]<dep[v]) swap(u,v);//保证u的层数大于等于v的层数
        for(int j=lg[maxdep-1];j>=0&&dep[u]>dep[v];j--)
            if(dep[fath[u][j]]>=dep[v]) u=fath[u][j];//让u向上跳
        if(u==v){//特判
            write(u);
            putchar('\n');
            continue;
        }
        for(int j=lg[dep[u]];j>=0;j--)//让两点同时向上跳
            if(fath[u][j]!=fath[v][j]){
                u=fath[u][j];
                v=fath[v][j];
            }
        write(fath[u][0]);
        putchar('\n');
    }

总代码:

#include<bits/stdc++.h>
#define N 500005
#define int long long
using namespace std;
vector<int>g[N];
int n,m,s,dep[N],fath[N][30],u,v,maxdep,lg[N];
bool vis[N];
int read(){
    int f=1,x=0;
    char ch=getchar();
    while(ch<'0'||ch>'9'){
        if(ch=='-') f=-1;
        ch=getchar();
    }
    while(ch>='0'&&ch<='9'){
        x=(x<<1)+(x<<3)+(ch^48);
        ch=getchar();
    }
    return f*x;
}
void write(int x){
    if(x<0) x=-x;
    if(x>=10) write(x/10);
    putchar(x%10^48);
}
void dfs(int now,int step){
    maxdep=max(maxdep,step);
    vis[now]=true;
    dep[now]=step;
    for(int i=0;i<g[now].size();i++)
        if(!vis[g[now][i]]){
            fath[g[now][i]][0]=now;
            dfs(g[now][i],step+1);
        }
    return;
}
signed main(){
    n=read();
    m=read();
    s=read();
    lg[1]=0;
    for(int i=2;i<=n;i++) lg[i]=lg[i>>1]+1;
    for(int i=1;i<n;i++){
        u=read();
        v=read();
        g[u].push_back(v);
        g[v].push_back(u);
    }
    dfs(s,1);
    for(int len=1;len<=lg[maxdep-1];len++)
        for(int i=1;i<=n;i++) fath[i][len]=fath[fath[i][len-1]][len-1];
    for(int i=1;i<=m;i++){
        u=read();
        v=read();
        if(dep[u]<dep[v]) swap(u,v);
        for(int j=lg[maxdep-1];j>=0&&dep[u]>dep[v];j--)
            if(dep[fath[u][j]]>=dep[v]) u=fath[u][j];
        if(u==v){
            write(u);
            putchar('\n');
            continue;
        }
        for(int j=lg[dep[u]];j>=0;j--)
            if(fath[u][j]!=fath[v][j]){
                u=fath[u][j];
                v=fath[v][j];
            }
        write(fath[u][0]);
        putchar('\n');
    }
    return 0;
}

tarjan 上的 dfs:

当然, 倍增有些人可能觉得有些难, 那么我再来补一下 tarjan 做法。

简介 tarjan

先放个图: 假设我要求 47410 的 LCA, 那么我们再建个图。 然后我们进行 dfs, 搜到哪里就在哪里打访问标记, 并确定父子关系。
等到遍历完成后, 再来到第二个图, 按照第一个图的父子关系进行查找。
详细流程如下:

建图

第一步还是建图, 不过这次我用结构体再建一遍。
代码如下:

void add(int x,int y){//原图
    t[++tot].to=y;
    t[tot].nxt=head[x];
    head[x]=tot;
    return;
}
void addx(int x,int y){//节点关系建图
    tx[++totx].to=y;
    tx[totx].nxt=headx[x];
    headx[x]=totx;
    return;
}
//以上均为模板,不再赘述
//主程序内调用
for(int i=1;i<n;i++){
   u=read();
   v=read();//原图
   add(u,v);
   add(v,u);
}
for(int i=1;i<=m;i++){
   u=read();
   v=read();
   addx(u,v);//节点之间建图
   addx(v,u);//注意,单向建图与双向建图会影响后面的代码
}

tarjan 操作

这里将结构体 dfs 模板修改一下即可。
详细解释看代码:

void tarjan(int now){
    vis[now]=true;//打访问标记,还是dfs的套路
    for(int i=head[now];i;i=t[i].nxt){//原图dfs
        int ta=t[i].to;
        if(vis[ta]) continue;//不重复查找
        tarjan(ta);//递归调用
        fa[ta]=now;//修改父子关系
    }
    for(int i=headx[now];i;i=tx[i].nxt){//结点关系dfs
        int ta=tx[i].to;
        if(vis[ta]){
            lca[i]=find(ta);//查找函数代码在下面
            if(i&1) lca[i+1]=lca[i];//这里就是刚刚说的不一样, 由于双向建边导致需要存两次, 不然会出错
            else lca[i-1]=lca[i];
        }
    }
    return;//结束
}

find 函数代码:

int find(int u){
    if(u!=fa[u]) fa[u]=find(fa[u]);
    return fa[u];
}//标准find函数

输出就不用看了吧
总代码:

#include<bits/stdc++.h>
#define N 500005
#define int long long
using namespace std;
struct trees{
    int to,nxt;
}t[N<<1],tx[N<<1];
int n,m,s,u,v,head[N<<1],tot,headx[N<<1],totx,fa[N<<1],lca[N<<1];
bool vis[N<<1];
int read(){
    int f=1,x=0;
    char ch=getchar();
    while(ch<'0'||ch>'9'){
        if(ch=='-') f=-1;
        ch=getchar();
    }
    while(ch>='0'&&ch<='9'){
        x=(x<<1)+(x<<3)+(ch^48);
        ch=getchar();
    }
    return f*x;
}
void write(int x){
    if(x<0) x=-x;
    if(x>=10) write(x/10);
    putchar(x%10^48);
}
void add(int x,int y){
    t[++tot].to=y;
    t[tot].nxt=head[x];
    head[x]=tot;
    return;
}
void addx(int x,int y){
    tx[++totx].to=y;
    tx[totx].nxt=headx[x];
    headx[x]=totx;
    return;
}
int find(int u){
    if(u!=fa[u]) fa[u]=find(fa[u]);
    return fa[u];
}
void tarjan(int now){
    vis[now]=true;
    for(int i=head[now];i;i=t[i].nxt){
        int ta=t[i].to;
        if(vis[ta]) continue;
        tarjan(ta);
        fa[ta]=now;
    }
    for(int i=headx[now];i;i=tx[i].nxt){
        int ta=tx[i].to;
        if(vis[ta]){
            lca[i]=find(ta);
            if(i&1) lca[i+1]=lca[i];
            else lca[i-1]=lca[i];
        }
    }
    return;
}
signed main(){
    n=read();
    m=read();
    s=read();
    for(int i=1;i<=n;i++) fa[i]=i;
    for(int i=1;i<n;i++){
        u=read();
        v=read();
        add(u,v);
        add(v,u);
    }
    for(int i=1;i<=m;i++){
        u=read();
        v=read();
        addx(u,v);
        addx(v,u);
    }
    tarjan(s);
    for(int i=1;i<=m;i++){
        write(lca[i<<1]);
        putchar('\n');
    }
    return 0;
}

两者对比

tarjan 时间复杂度为 O(n+m), 而倍增为 O(n\log_2n),tarjan 更优。
但其致命缺陷也很明显, 就是无法在线回答