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;
}

 

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

 

posted @ 2019-08-07 11:12  GMSD  阅读(209)  评论(0编辑  收藏  举报