zoukankan      html  css  js  c++  java
  • LCA算法的介绍与模板

    对于一棵树,求两个节点的最近公共祖先(LCA)。

      如下图:(以下数字代表对应编号的节点)

      11 和 66 的 LCA 是 88 。

      1111 和 11 的 LCA 是 88 。

      1111 和 1515 的 LCA 是 44 。

      1414 和 1313 的 LCA 是 11 。

    以洛谷P3379模板题为例

    方法一:用倍增来在线求 LCA ,时间和空间复杂度分别是 O((n+q)logn)O((n+q)log⁡n) 和 O(nlogn)O(nlog⁡n) 。

      对于这个算法,我们从最暴力的算法开始:

        ①如果 aa 和 bb 深度不同,先把深度调浅,使他变得和浅的那个一样

        ②现在已经保证了 aa 和 bb 的深度一样,所以我们只要把两个一起一步一步往上移动,直到他们到达同一个节点,也就是他们的最近公共祖先了。

    #include<iostream>
    #include<cstring>
    #include<algorithm>
    #include<vector> 
    using namespace std;
    const int maxn=5e5+5;
    int lg[maxn];//log 2n向下取整
    const int maxbit=20;
    vector<int>G[maxn];
    int depth[maxn];//记录每个节点的深度
    int father[maxn][maxbit];//father[i][j]指的是i节点往上2^j的节点 
    void dfs(int nowp,int fa)
    {
        depth[nowp]=depth[fa]+1;//当前节点深度是父节点深度+1
        father[nowp][0]=fa;
        for(int j=1;j<=lg[depth[nowp]];j++)//倍增求father 
         father[nowp][j]=father[father[nowp][j-1]][j-1];
        for(int i=0;i<G[nowp].size();i++)
        {
            if(G[nowp][i]!=fa)
            dfs(G[nowp][i],nowp);
        }
    }
    int LCA(int u,int v)
    {
        if(depth[u]<depth[v])//维护u的深度最大
          swap(u,v);
         while(depth[u]!=depth[v])
         u=father[u][lg[depth[u]-depth[v]]];//使两个节点在同一高度;
         if(u==v)
         return u;
         for(int j=lg[depth[u]];j>=0;--j)
         {
             if(father[u][j]!=father[v][j])
             {
                 u=father[u][j];
                 v=father[v][j];
            }
         } //得到最近公共祖先下一位;
         return father[u][0]; 
    }
    int main()
    {
        lg[0]=-1;
        for(int i=1;i<maxn;i++)
        lg[i]=lg[i>>1]+1; 
        int n,m,s;int x,y;
        scanf("%d%d%d",&n,&m,&s);
        for(int i=1;i<n;i++)
        {
            
            scanf("%d%d",&x,&y);
            G[x].push_back(y);
            G[y].push_back(x);
        }
        dfs(s,0);//假设根结点的父节点为0;
        while(m--)
        {
            scanf("%d%d",&x,&y);
            printf("%d
    ",LCA(x,y)); 
         } 
        return 0;
    } 

     方法二:

    现在来介绍一种 O(nlogn)O(nlog⁡n) 预处理,O(1)O(1) 在线查询的算法。

      RMQ 的意思大概是“区间最值查询”。顾名思义,用 RMQ 来求 LCA 是通过 RMQ 来实现的。

      首先,您可以了解一下 dfs 序。链接:http://www.cnblogs.com/zhouzhendong/p/7264132.html

      在 dfs 的过程中,退出一个子树之后就不会再进入了。这是个很好的性质。

      所以很显然,一个子树中深度最浅的节点必定是该子树的树根。

      显然,两个节点的 LCA 不仅是两个节点的最近公共祖先,而且是囊括这两个节点的最小子树的根,即囊括这两个节点的最小子树中的深度最小的节点。

      我们来想一想如何得到这个节点。

      现在,我们稍微修改一下 dfs 序,搞一个欧拉序。

      欧拉序,就是每次从 father(x)father(x) 进入节点 xx 或者从子节点回溯到 xx 都要把 xx 这个编号扔到一个数组的最后。

      这样最终会得到一个长度约为 2n2n 的数列。(考虑每一个节点贡献为 22 ,分别是从其父亲进入该节点,和从该节点回到其父亲)

      

      例如,上图这棵树的一个欧拉序为 8,5,9,5,8,4,6,15,6,7,6,4,10,11,10,16,3,16,12,16,10,2,10,4,8,1,14,1,13,1,88,5,9,5,8,4,6,15,6,7,6,4,10,11,10,16,3,16,12,16,10,2,10,4,8,1,14,1,13,1,8 。

      建议跟着我给出的欧拉序走一遍,再次理解欧拉序的含义。

      再注意到,一对点的 LCA 不仅是囊括这两个节点的最小子树中的深度最小的节点,还是连接这对点的简单路径上深度最小的点。

      而且从离开 aa 到进入 bb 的这段欧拉序必然包括所有这对点之间的简单路径上的所有点,所以我们考虑求得这段欧拉序中所包含的节点中的 深度最小的点即其 LCA 。

      从 aa 到 bb 的这段欧拉序会包含这棵子树中的其他节点,但是不会影响这个最浅点的求得,因为“一对点的 LCA 是囊括这两个节点的最小子树中的深度最小的节点”。

      显然, aa 到 bb 这段欧拉序是个连续区间。

      你可以用线段树维护,但是线段树太 low 了。

      现在我们考虑通过预处理来 O(1)O(1) 获得这个最浅点。

      于是我们要学习一个叫做 ST表 的东西来搞定这个。(和之前倍增中处理的 fafa 数组差不多)

      我再放一篇大佬博客来介绍 RMQ 与 ST表 :  https://blog.csdn.net/qq_31759205/article/details/75008659

    代码:

    #include<iostream>
    #include<algorithm>
    #include<vector>
    #include<cstring>
    using namespace std;
    const int maxn=5e5+5;
    const int maxbit=20;
    vector<int>G[maxn];
    int order[maxn*2];//dfs序
    int depth[maxn*2];//dfs序对应点深度 
    int st[maxn*2][maxbit];//st[i][j]指的是dfs序包含i在内的2^j个点深度最小的点的编号 
    int first_place[maxn];//储存dfs序每一个结点最早出现的位置 
    int lg[maxn*2]; 
    inline int read()//读入优化 
    {
        char ch=getchar();
        int x=0,f=1;
        while((ch>'9'||ch<'0')&&ch!='-')
         ch=getchar();
         if(ch=='-')
         {
             f=-1;ch=getchar();
          } 
          while('0'<=ch&&ch<='9')
          {
              x=x*10+ch-'0';
              ch=getchar();
          }
          return x*f;
    }
    int cnt=0;//dfs序长度
    void dfs(int nowp,int dep)//nowp当前点,dep父节点深度
    {
        ++cnt;
        first_place[nowp]=cnt;//每个点进一次dfs,所以有这个 
        order[cnt]=nowp;//dfs第几个存nowp节点
        depth[cnt]=dep+1; 
        for(int i=0;i<G[nowp].size();i++)
        {
            int to=G[nowp][i];
            if(first_place[to]==0)//不是父节点
            {
                dfs(to,dep+1);
                ++cnt;
                order[cnt]=nowp;
                depth[cnt]=dep+1; 
            } 
        }
     } 
    void st_init()//求st表 0(nlog(n))
     {
         for(int i=1;i<=cnt;i++)
         st[i][0]=i;
         int a,b;
         for(int j=1;j<=lg[cnt];j++)
          {
              for(int i=1;i+(1<<j)-1<=cnt;i++)
              {
                  a=st[i][j-1];
                  b=st[i+(1<<(j-1))][j-1];
                  if(depth[a]<depth[b])
                  st[i][j]=a;
                  else
                  st[i][j]=b;
            }
        }
     }
    int main()
    {
        lg[0]=-1;
        for(int i=1;i<maxn*2;i++)
        lg[i]=lg[i>>1]+1;
        int n,m,s;
        n=read();
        m=read();
        s=read();
        int x,y;
        for(int i=1;i<n;i++)
        {
            x=read();
            y=read();
            G[x].push_back(y);
            G[y].push_back(x);
        }
        dfs(s,0);
        st_init();
        while(m--)
        {
            x=read();
            y=read();
            x=first_place[x];
            y=first_place[y];
            if(x>y)
            swap(x,y);
            int k=lg[y-x];
            int a,b;
            a=st[x][k];
            b=st[y-(1<<k)+1][k];
            if(depth[a]<depth[b])
            printf("%d
    ",order[a]);
            else
            printf("%d
    ",order[b]);
        }
        return 0;
    }

    方法三:

    LCA_Tarjan

      TarjanTarjan 算法求 LCA 的时间复杂度为 O(n+q)O(n+q) ,是一种离线算法,要用到并查集。(注:这里的复杂度其实应该不是 O(n+q)O(n+q) ,还需要考虑并查集操作的复杂度 ,但是由于在多数情况下,路径压缩并查集的单次操作复杂度可以看做 O(1)O(1),所以写成了 O(n+q)O(n+q) 。)

      TarjanTarjan 算法基于 dfs ,在 dfs 的过程中,对于每个节点位置的询问做出相应的回答。

      dfs 的过程中,当一棵子树被搜索完成之后,就把他和他的父亲合并成同一集合;在搜索当前子树节点的询问时,如果该询问的另一个节点已经被访问过,那么该编号的询问是被标记了的,于是直接输出当前状态下,另一个节点所在的并查集的祖先;如果另一个节点还没有被访问过,那么就做下标记,继续 dfs 。

      当然,暂时还没那么容易弄懂,所以建议结合下面的例子和标算来看看。

    (下面的集合合并都用并查集实现)

      比如:8114138−1−14−13 ,此时已经完成了对子树 11 的子树 1414 的 dfsdfs 与合并( 1414 子树的集合与 11 所代表的集合合并),如果存在询问 (13,14)(13,14) ,则其 LCA 即 getfather(14)getfather(14) ,即 11 ;如果还存在由节点 1313 与 已经完成搜索的子树中的 节点的询问,那么处理完。然后合并子树 1313 的集合与其父亲 11 当前的集合,回溯到子树 11 ,并深搜完所有 11 的其他未被搜索过的儿子,并完成子树 11 中所有节点的合并,再往上回溯,对节点 11 进行类似的操作即可。

    #include<iostream>
    #include<vector>
    #include<cstring>
    using namespace std;
    const int maxn=5e5+5;
    struct node
    {
        int x,y,lca;
    }query[maxn];
    int fa[maxn];
    int vis[maxn];
    vector<int>G[maxn];
    vector<int>Q[maxn];
     int find(int x)
     {
         if(fa[x]==x)
         return x;
         else
         return fa[x]=find(fa[x]);
     }
    void dfs(int u)
    {
        vis[u]=1;
        for(int i=0;i<Q[u].size();i++)
        {
            int qid=Q[u][i];
            if(query[qid].x==u)
            {
                if(vis[query[qid].y])
                {
                    query[qid].lca=find(query[qid].y);
                }
            }
            else
            {
            if(vis[query[qid].x])
            query[qid].lca=find(query[qid].x);
          }
        }
        for(int i=0;i<G[u].size();i++)
        {
            int v=G[u][i];
            if(vis[v])
            continue;
            else
            {
                dfs(v);
                fa[v]=u;
            }
        }
    }
    int main()
    {
        int n,m,s;
        for(int i=1;i<maxn;i++)
        fa[i]=i;
        scanf("%d%d%d",&n,&m,&s);
        int x,y;n--;
        while(n--)
        {
            scanf("%d%d",&x,&y);
            G[x].push_back(y);
            G[y].push_back(x);
        }
        for(int i=1;i<=m;i++)
        {
            scanf("%d%d",&query[i].x,&query[i].y);
            Q[query[i].x].push_back(i);
             Q[query[i].y].push_back(i);
        }
        dfs(s);
        for(int i=1;i<=m;i++)
        {
            printf("%d
    ",query[i].lca);
        }
        return 0;
    }
  • 相关阅读:
    前端 CSS
    前端 HTML
    前端 JavaScript 初识
    网络编程——线程池
    网络编程——同一进程中的队列(多线程)
    网络编程——进程间的共享内存
    vue实现前端简易版模糊查询
    封装axios请求拦截器
    关于node中 mysql Client does not support authentication protocol requested by server; consider upgrading MySQL client 解决方法
    封装一个时间方法
  • 原文地址:https://www.cnblogs.com/flyljz/p/11685402.html
Copyright © 2011-2022 走看看