zoukankan      html  css  js  c++  java
  • LCA——最近公共祖先

    首先是最近公共祖先的概念(什么是最近公共祖先?)(摘自https://www.cnblogs.com/JVxie/p/4854719.html):

    在一棵没有环的树上,每个节点肯定有其父亲节点和祖先节点,而最近公共祖先,就是两个节点在这棵树上深度最大公共祖先节点

    换句话说,就是两个点在这棵树上距离最近的公共祖先节点

    所以LCA主要是用来处理当两个点仅有唯一一条确定的最短路径时的路径。

    有人可能会问:那他本身或者其父亲节点是否可以作为祖先节点呢?

    答案是肯定的,很简单,按照人的亲戚观念来说,你的父亲也是你的祖先,而LCA还可以将自己视为祖先节点

    那么求LCA有两种方法,一种是倍增,另一种是Tarjan(好吧,还有RMQ,只不过我不会)

    这里以此题为例子

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

    简略的讲一下题目大意,在一个树中间,求出两个点之间的最小公共祖先

    首先倍增

    先讲一下暴力做法:1.求出两个点的深度,假设两个点分别是a,b ,深度为x,y,且x>y

                                     2.将点a向上跳,知道跳到与b一个深度

                                     3.做完第二步之后,将两个点一起往上跳,知道重合为止

                                     4.那么这个点就是这两个点的LCA

    这样做肯定是可以的,但是时间复杂度太高了,可以卡到n^2

    那么我们可以把第二步优化,显然就用倍增(显然大法好)

    先讲一下倍增的意思

    有一个数字n,n=123

    那么n=64+32+16+8+2+1=2^6+2^5+2^4+2^3+2^1+2^0

    可以得出对于任何自然数n,都可以进行这样的拆分,因为可以看出就是二进制

    回归正题

    设一个数组f[ ][ ],f[i][j]表示i这个节点的2^j祖先

    那么可以得出f[i][j]=f[f[i][j-1]][j-1]

    因为f[i][j-1]表示i的2^j-1祖先,那么这个点的2^j-1祖先就是i的2^j祖先

    运用BFS就可以求出深度(这个不用说吧……),再求出f的值(t为log2(n),因为这个点的最大祖先不会超过2^t)

    void Bfs(){
        q.push(root);deep[root]=1;
        while(!q.empty()){
            int x=q.front();q.pop();
            for(int i=head[x];i;i=next[i]){
                int y=ver[i];
                if(deep[y])continue;
                deep[y]=deep[x]+1;
                f[y][0]=x;
                for(int j=1;j<=t;j++)f[y][j]=f[f[y][j-1]][j-1];
                q.push(y);
            }
        }
    }

    既然已经得出f值,那么在运用常规方法

    int Get_LCA(int x,int y){
        if(deep[x]<deep[y])swap(x,y);//假设x深度大于y
        for(int i=t;i>=0;i--)
           if(deep[f[x][i]]>=deep[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];
    }

    接下来是完整代码:

    #include<bits/stdc++.h>
    using namespace std;
    
    const int N=500002;
    int number,m,root,f[N][20],next[N*2],head[N*2],ver[N*2],tot=0,res[N],deep[N],t;
    queue<int> q;
    
    int read(){    
        int s=0,w=1;char ch=getchar();
        while(ch<'0'||ch>'9')w=(ch=='-')?-1:1,ch=getchar();
        while(ch>='0'&&ch<='9')s=s*10+ch-'0',ch=getchar();
        return s*w;
    }
    
    void add(int x,int y){
        ver[++tot]=y;next[tot]=head[x];head[x]=tot;
    }
    
    void Bfs(){
        q.push(root);deep[root]=1;
        while(!q.empty()){
            int x=q.front();q.pop();
            for(int i=head[x];i;i=next[i]){
                int y=ver[i];
                if(deep[y])continue;
                deep[y]=deep[x]+1;
                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 Get_LCA(int x,int y){
        if(deep[x]<deep[y])swap(x,y);
        for(int i=t;i>=0;i--)
           if(deep[f[x][i]]>=deep[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(){
        number=read();m=read();root=read();
        for(int i=1;i<number;i++){
            int x=read(),y=read();
            add(x,y);add(y,x);
        }
        t=(int)((log(number)/log(2))+1);
        Bfs();
        for(int i=1;i<=m;i++){
            int x=read(),y=read();
             cout<<Get_LCA(x,y)<<endl;
        }
        return 0;
    }

    好的,再是Tarjan

    不得不说,Tarjan挺厉害的,可以去度娘上搜一下,他发明了很多算法,所以很多时候,算法名都叫Tarjan,但是内容完全不一样

    主要思想为:1.任选一个点为根节点,从根节点开始。

           2.遍历该点u所有子节点v,并标记这些子节点v已被访问过。

           3.若是v还有子节点,返回2,否则下一步。

           4.合并v到u上。

           5.寻找与当前点u有询问关系的点v。

           6.若是v已经被访问过了,则可以确认u和v的最近公共祖先为v被合并到的父亲节点a。

    以上这个还是摘自https://www.cnblogs.com/JVxie/p/4854719.html,大家可以去看看

    不过看的时候我也没看懂,学这种东西最好的方法就是拿纸和笔模拟:

    以此图为例:

    假设求(3,6)(4,5)(4,7)的LCA,有关系表示要求的点,就像3与6有关系,4和5有关系

    res[i]=1表示当前的点已经被寻找过,,=0表示还没有遍历到这个点

    以1为根,找子节点,先找2,2再找子节点,先找3,那么res[1]=1,res[2]=1;

    此时3没有了子节点,那么寻找与其有关系点的点,有一个6

    但是res[6]=0,跳过,将这个点与2合并(这里就用的并查集,没学过可以看其他人的博客),father[3]=2

    回溯到2,res[3]=1,在寻找4,4有一个儿子5,寻找五,标记res[4]=1;

    合并father[5]=4,那么4和5的LCA=find(5)(并查集的查找操作)

    继续向上回溯,合并,一直到6

    合并1,6,发现3和6有关系,res[3]=1,3和6的LCA为find(3);res[6]=1;

    寻找到7,合并7和6,发现4和7有关系,4和7的LCA为find(7);

    最后就得出来所有答案,时间复杂度为O(n+m);

    但是这个是离线算法,就是不能一个一个求,要一起求完

    核心代码:

    int find(int a){
        if(father[a]!=a)father[a]=find(father[a]);
        return father[a];
    }
    
    void Tanjan(int x){
        res[x]=1;
        for(int i=head[x];i;i=next[i]){
            int y=ver[i];
            if(res[y])continue;
            Tanjan(y);father[find(y)]=find(x);
        }
        for(int i=0;i<son[x].size();i++){
            int y=son[x][i];
            if(res[y]==2)ans[xb[x][i]]=find(y);
        }
        res[x]=2;
    }

    再是完整代码

    #include<bits/stdc++.h>
    using namespace std;
    
    const int N=500000+2;
    int number,m,root,next[N*2],head[N*2],ver[N*2],tot=0;
    int res[N],father[N],ans[N];
    vector<int> son[N],xb[N];
    
    int read(){    
        int s=0,w=1;char ch=getchar();
        while(ch<'0'||ch>'9')w=(ch=='-')?-1:1,ch=getchar();
        while(ch>='0'&&ch<='9')s=s*10+ch-'0',ch=getchar();
        return s*w;
    }
    
    void add(int x,int y){
        ver[++tot]=y;next[tot]=head[x];head[x]=tot;
    }
    
    int find(int a){
        if(father[a]!=a)father[a]=find(father[a]);
        return father[a];
    }
    
    void Tanjan(int x){
        res[x]=1;
        for(int i=head[x];i;i=next[i]){
            int y=ver[i];
            if(res[y])continue;
            Tanjan(y);father[find(y)]=find(x);
        }
        for(int i=0;i<son[x].size();i++){
            int y=son[x][i];
            if(res[y]==2)ans[xb[x][i]]=find(y);
        }
        res[x]=2;
    }
    
    int main(){
        number=read();m=read();root=read();
        for(int i=1;i<number;i++){
            int x=read(),y=read();
            add(x,y);add(y,x);
        }
        for(int i=1;i<=m;i++){
            int x=read(),y=read();ans[i]=1e9;
            son[x].push_back(y);son[y].push_back(x);
            xb[x].push_back(i);xb[y].push_back(i);
        }
        for(int i=1;i<=number;i++)father[i]=i;
        Tanjan(root);
        for(int i=1;i<=m;i++)cout<<ans[i]<<endl;
        return 0;
    }

    能力有限,如果没有讲清楚,可以看之前的网址,那篇博客写的很好

  • 相关阅读:
    java基础多线程
    java反射基础
    JSP-4(Session)
    JSP-3
    JSP-2
    复试计算机专业文献翻译
    jsp
    实现输入输出对应模型
    servlet
    tomcat的入门(1)
  • 原文地址:https://www.cnblogs.com/GMSD/p/11314135.html
Copyright © 2011-2022 走看看