最近公共祖先(LCA)问题

Posted stungyep

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了最近公共祖先(LCA)问题相关的知识,希望对你有一定的参考价值。

最近公共祖先

定义:给定一颗有根树,若结点 z 既是 x 的祖先,也是 y 的祖先,则称 z 是 x,y 的公共祖先。在 x,y 所有的公共祖先中,深度最大的一个称为 x,y 的最近公共祖先,简称(LCA(x,y))

求解最近公共祖先一般有三种解法:向上标记法,树上倍增法和 Tarjan 算法。

1.向上标记法

即对于任何两个结点 x , y ,分别从x y 向上走并标记它们所有经过的节点,第一次相遇的节点即为最近公共祖先。其时间复杂度最坏为(O(n^2))

2.树上倍增法

树上倍增法在很多问题都有很广泛的应用,当然最近公共祖先也可以用树上倍增算法来求,其基本思想是:

设d[x]>=d[y],其中d[x]表示x的深度,然后利用二进制思想,将x不断调整直至和y在同一深度,检查此时x和y是否相等,若相等即为最近公共祖先,否则将x,y一起向上调整直至相等,此时则为最近公共祖先。

该算法时间复杂度为:(O((n+m)logn))具体代码(裸题,求权值)为:

#include<bits/stdc++.h>

using namespace std;
const int maxn=5e5+10;
int head[maxn],dis[maxn],d[maxn],f[maxn][20];
struct Edge
{
    int nex,to,val;
}edge[maxn<<1];
int T,t,n,m,tot;
queue<int> q;
void init()
{
    tot=0;
    memset(head,-1,sizeof(head));
    memset(d,0,sizeof(d));
}
void add(int from,int to,int val)
{
    edge[++tot].to=to;
    edge[tot].val=val;
    edge[tot].nex=head[from];
    head[from]=tot;
}
void bfs()
{
    q.push(1);
    d[1]=1;
    while(!q.empty())
    {
        int x=q.front();
        q.pop();
        for(int i=head[x];i!=-1;i=edge[i].nex)
        {
            int y=edge[i].to;
            if(d[y])    continue;
            d[y]=d[x]+1;
            dis[y]=dis[x]+edge[i].val;
            f[y][0]=x;
            for(int j=1;j<=t;++j)
                f[y][j]=f[f[y][j-1]][j-1];
            q.push(y);
        }
    }
}
int lca(int x,int y)
{
    if(d[x]<d[y])   swap(x,y);
    for(int i=t;i>=0;--i)
        if(d[f[x][i]]>=d[y])    x=f[x][i];
    if(x==y)   return x;
    for(int i=t;i>=0;--i){
        if(f[x][i]!=f[y][i]){
            x=f[x][i];
            y=f[y][i];
        }
    }
    return f[x][0];    
}

int main()
{
    scanf("%d",&T);
    while(T--)
    {
        scanf("%d %d",&n,&m);
        t=(int)(log(n)/log(2))+1;
        init();
        for(int i=1;i<n;++i){
            int a,b,val;
            scanf("%d %d %d",&a,&b,&val);
            add(a,b,val);
            add(b,a,val);
        }
        bfs();
        for(int i=1;i<=m;++i){
            int x,y;
            scanf("%d %d",&x,&y);
            printf("%d
",dis[x]+dis[y]-2*dis[lca(x,y)]);
        }
    }
}

3.Tarjan算法

Tarjan算法本质上是用并查集对“向上标记法”的优化。它是一个离线算法(即将所有输入都输入完成后一起输出);其时间复杂度为:(O(n+m))

该算法将树中所有结点分成了三类:访问并回溯过,访问但为回溯过,未访问三类。该三类点分别标记为2,1和0。该算法的思想是当一个结点被标记为2时,则将它所在集合合并到它的父节点所在的集合中,这就相当于每个完成回溯的结点都有一个指针指向其父节点,所以只需查询y所在集合的代表元素,就等价与从y一直向上走到一个开始递归但尚未回溯的结点,即(LCA(x,y))

还是上面的题面,看代码:

#include<bits/stdc++.h>

using namespace std;
const int maxn=5e4+10;
const int maxe=210;
const int inf=0x3f3f3f3f;
struct Edge
{
    int nex,to,val;
}edge[maxn<<1];
int T,n,m,t,tot;
int head[maxn],fa[maxn],v[maxn],dis[maxn],ans[maxe];
vector<int> query[maxn],query_id[maxn];
void init()
{
    tot=0;
    memset(head,-1,sizeof(head));
    memset(dis,0,sizeof(dis));
    memset(v,0,sizeof(v));
    for(int i=1;i<=n;++i){   
        fa[i]=i;
        query[i].clear();
        query_id[i].clear();
    }
}
void add(int from,int to,int val)
{
    edge[++tot].to=to;
    edge[tot].val=val;
    edge[tot].nex=head[from];
    head[from]=tot;
}
void add_query(int x,int y,int id)
{
    query[x].push_back(y);
    query_id[x].push_back(id);
    query[y].push_back(x);
    query_id[y].push_back(id);
}
int ffind(int x)
{
    return x==fa[x]?x:fa[x]=ffind(fa[x]);
}
void tarjan(int x)
{
    v[x]=1;
    for(int i=head[x];i!=-1;i=edge[i].nex)
    {
        int y=edge[i].to;
        if(v[y])  continue;
        dis[y]=dis[x]+edge[i].val;
        tarjan(y);
        fa[y]=x;
    }
    for(int i=0;i<query[x].size();++i)
    {
        int y=query[x][i],id=query_id[x][i];
        if(v[y]==2){
            int lca=ffind(y);
            ans[id]=min(ans[id],dis[x]+dis[y]-2*dis[lca]);
        }
    }
    v[x]=2;
}

int main()
{
    scanf("%d",&T);
    while(T--)
    {
        scanf("%d %d",&n,&m);
        init();
        for(int i=1;i<n;++i){
            int a,b,val;
            scanf("%d %d %d",&a,&b,&val);
            add(a,b,val);
            add(b,a,val);
        }
        for(int i=1;i<=m;++i){
            int x,y;
            scanf("%d %d",&x,&y);
            if(x==y)    ans[i]=0;
            else{
                add_query(x,y,i);
                ans[i]=inf;
            }
        }
        tarjan(1);
        for(int i=1;i<=m;++i)
            printf("%d
",ans[i]);
    }
    system("pause");
}

以上是关于最近公共祖先(LCA)问题的主要内容,如果未能解决你的问题,请参考以下文章

LCA 最近公共祖先

二叉树最近公共祖先

二叉树最近公共祖先

『图论』LCA 最近公共祖先

『图论』LCA最近公共祖先

LCA —— 最近公共祖先