树论讲解——最近公共祖先(lca)

冷暖自知 提交于 2020-01-27 18:48:51

最近公共祖先?!

有人肯定要问:什么是最近公共祖先???!!

好那我们现在就来说说什么是最近公共祖先吧!

最近公共祖先有一个好听的名字叫——lca

这是一种算法,这个算法基于并查集和深度优先搜索。算法从根开始,对每一棵子树进行深度优先搜索,访问根时,将创建由根结点构建的集合,然后对以他的孩子结点为根的子树进行搜索,使对于 u, v 属于其某一棵子树的 LCA 询问完成。这时将其所有子树结点与根结点合并为一个集合。 对于属于这个集合的结点 u, v 其 LCA 必定是根结点。

 

对于有根树T的两个结点u、v,最近公共祖先LCA(T,u,v)表示一个结点x,满足x是u、v的祖先且x的深度尽可能大。

怎么求两个已知点的LCA呢·?

有一个比较暴力的想法:先将这两个点的路径上的所经过的所有点,然后再从根节点向下找第一个分叉的点,这个点就是这两个点的最近公共祖先。

还有一个想法:先将两个深度不同的点转化成深度相同的点,然后再将这两个点一起向上跳,直到找到同一个点。

        §  one。倍增法

何为倍增法?

倍增法就是我们先把深度不同的两个点转化成深度相同的点。然后再对这两个点同时倍增。

这种做法我们先用一个数组fa[x]【y】数组来存第x个节点的2^y的父亲节点。

这样我们就能在o(lg n)的时间内查询任意一个点的lca。

所以我们还是采用上面所述的那种做法,现将深度不同的两个点转化成深度相同的两个点。

然后再对两个点同时进行倍增。

好那我们下面来求一求给定两点:x,y的最近公共祖先吧!

代码1:

 

#include<vector>
#include<stdio.h>
#include<cstdlib>
#include<cstring>
#include<iostream>
#include<algorithm>
#define N 500001
#define maxn 123456
using namespace std;
vector<int>vec[N];
int n,x,y,fa[N][20],deep[N],m,root;
void dfs(int x)
{
    deep[x]=deep[fa[x][0]]+1;
    for(int i=0;fa[x][i];i++)
      fa[x][i+1]=fa[fa[x][i]][i];
    for(int i=0;i<vec[x].size();i++)
    {
        if(!deep[vec[x][i]])
        {
            fa[vec[x][i]][0]=x;
            dfs(vec[x][i]);
         } 
    }
}
int lca(int x,int y)
{
    if(deep[x]>deep[y])
      swap(x,y);//省下后面进行分类讨论,比较方便 
    for(int i=18;i>=0;i--)
    {
        if(deep[fa[y][i]]>=deep[x]) 
          y=fa[y][i];//让一个点进行倍增,直到这两个点的深度相同 
    }
    if(x==y) return x;//判断两个点在一条链上的情况 
    for(int i=18;i>=0;i--)
    {
        if(fa[x][i]!=fa[y][i])
        {
            y=fa[y][i];
            x=fa[x][i];
          }  
    }
    return fa[x][0];//这样两点的父亲就是他们的最近公共祖先 
}
int main()
{
    scanf("%d%d%d",&n,&m,&root);
    for(int i=1;i<n;i++)
    {
        scanf("%d%d",&x,&y);
        vec[x].push_back(y);
        vec[y].push_back(x);
    }
    deep[root]=1;
    dfs(root);
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d",&x,&y);
        printf("%d\n",lca(x,y));
    }
    return 0;
}

 

                                   §   two。树剖法。

 

 看到这个算法,肯定有想问树剖法是个什么鬼?

树抛嘛,顾名思义肯定是将一棵树进行剖分。

树链抛分的核心是划分一棵树的重边和轻边。

我们把一个节点所拥有的子节点的个数记为size

对于一棵树来说,一个节点u和他的父节点v之间一定只有一条重边。这条重边连向她的儿子中size值最大的节点。

这样一棵树的所有重边就组成了一条重链。

对于节点x我们记录x的重边的顶点top x.

加入一个节点到另一条节点有轻边,那这条轻边满足:2*size u<size V

因此,根到一棵树的节点上最多有long n 条轻边

现在我们要用树链剖分来求两节点x,y的lca

首先我们要先判断两个节点的top那个大

我们把top值小的节点改成fa[top[x]]

重复上述两个过程直到top[x]=top[y]

最后将上述两个节点中深度较小的值作为这两点的lca。

 下面给出本人代码

#include<vector>
#include<stdio.h>
#include<cstdlib>
#include<cstring>
#include<iostream>
#include<algorithm>
#define N 500001
#define maxn 123456
using namespace std;
vector<int>vec[N];
int n,m,root,x,y,fa[N],deep[N],size[N],top[N];
int lca(int x,int y)
{
    for( ;top[x]!=top[y];)
    {
        if(deep[top[x]]<deep[top[y]])
         swap(x,y);
        x=fa[x];
    }
    if(deep[x]>deep[y])
     swap(x,y);
    return x;
 } 
void dfs(int x)
{
    size[x]=1;
    deep[x]=deep[fa[x]]+1;
    for(int i=0;i<vec[x].size();i++)
    {
        if(fa[x]!=vec[x][i])
        {
            fa[vec[x][i]]=x;
            dfs(vec[x][i]);
            size[x]+=size[vec[x][i]];
        }
    }
}
void dfs1(int x)
{
    int t=0;
    if(!top[x])  top[x]=x;
    for(int i=0;i<vec[x].size();i++)
      if(vec[x][i]!=fa[x]&&size[vec[x][i]]>size[t])
         t=vec[x][i];
    if(t) 
    {
        top[t]=top[x];
        dfs1(t);
    }
    for(int i=0;i<vec[x].size();i++)
     if(vec[x][i]!=fa[x]&&vec[x][i]!=t)
      dfs1(vec[x][i]);
}
int main()
{    scanf("%d%d%d",&n,&m,&root);
    for(int i=1;i<n;i++)
    {
        scanf("%d%d",&x,&y);
        vec[x].push_back(y);
        vec[y].push_back(x);
    }
    dfs(root);
    dfs1(root);
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d",&x,&y);
        printf("%d\n",lca(x,y));
    }
    return 0;
}

 

          §   three。并查集

 

对于一棵树的每个节点父亲的查询问题,我们可以采用并查集的方法。

我们先用一个数组fa[x]来储存x的父亲节点,每个点与他的父亲节点在一个并查集里。

如果一个点满足:fa[x]=x,则说明x是这棵树的根节点。

我们在查询两个节点是否在一个并查集中时,只要查询这两个节点的所在子树根节点是否相同即可。

在合并两个字数的集合时,我们只要讲fa[root[x]]变成root[x]即可。

有没有感觉这个方法教前面的几个方法来说要简单很多?

好,既然这么简单,我们就直接上代码吧!

#include<vector>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<iostream>
#include<algorithm>
#define N 10000
using namespace std;
int n,m,t,fa[N],x,y;
int find(int x)
{
    return fa[x]==x?x:fa[x]=find(x);
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
     fa[i]=i;
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d%d",&t,&x,&y);
        if(t==1) fa[find(x)]=find(y);
        else
        {
            if(find(x)==find(y)) printf("YES");
            else printf("NO");
        }
    }
    return 0;
}

        §  four。tarjian法(简称塔尖)

与之前的树抛和倍增法不同,tarjian算是一种离线算法

我们需要将米一组询问用vec储存下来,将其挂在改组询问询问的两个节点上。

之后遍历整棵树。在访问一个节点x时,我们设置这个节点的fa【x】=x;只有在询问完时,我们将这个点的fa【x】设置城dad【x】。

之后我们在询问一个节点时,枚举过于这个点的所有询问。若果询问中的另一个节点已经被访问过,那么这两个点的lca就是已经被访问过的这个点。

代码

#include<vector>
#include<stdio.h>
#include<cstring>
#include<cstdlib>
#include<iostream>
#include<algorithm>
#define N 500001
using namespace std;
vector<int>vec[N],que[N];
int n,m,qx[N],qy[N],x,y,root,fa[N],dad[N],ans[N];
int find(int x)
{
    return fa[x]==x?x:fa[x]=find(fa[x]);
}
void dfs(int x)
{
    fa[x]=x;
    for(int i=0;i<vec[x].size();i++)
     if(vec[x][i]!=dad[x])
      dad[vec[x][i]]=x,dfs(vec[x][i]);
    for(int i=0;i<que[x].size();i++)
      if(dad[y=qx[que[x][i]]^qy[que[x][i]]^x])
       ans[que[x][i]]=find(y);
    fa[x]=dad[x];
}
int main()
{
    scanf("%d%d%d",&n,&m,&root);
    for(int i=1;i<n;i++)
    {
        scanf("%d%d",&x,&y);
        vec[x].push_back(y);
        vec[y].push_back(x);
    }
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d",&qx[i],&qy[i]);
        que[qx[i]].push_back(i);
        que[qy[i]].push_back(i);
    }
    dfs(root);
    for(int i=1;i<=m;i++)
     printf("%d\n",ans[i]);
    return 0;
 } 

 

 

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!