最近公共祖先(LCA)算法实现过程 【Tarjan离线+倍增在线+RMQ】

最近公共祖先(LCA)


首先来介绍下最近公共祖先(LCA)的概念

  1. 百度上的解释:对于有根树T的两个结点u、v,最近公共祖先LCA(T,u,v)表示一个结点x,满足x是u、v的祖先且x的深度尽可能大。
  2. 通俗语言:在一棵没有环的树上,每个节点肯定有其父亲节点和祖先节点,而最近公共祖先节点,就是两个节点在这棵树上深度最大的公共的祖先节点,即两个点在这棵树上距离最近的公共祖先节点。易知,在树上这两个点的最短路径唯一时,一定经过该公共祖先节点。(父亲节点也是祖先节点,另外某个节点本身也是它的祖先节点)


        给出个例子加深理解,如图,3和5的最近公共祖先为1,5和6的最近公共祖先为2,2和7的最近公共祖先为2, 6和7的最近公共祖先为4。


    求公共最近祖先的算法(设询问次数为q)
    1. 暴力(实际做题不可行):对于每个询问,遍历所有的点,时间复杂度为O(n*q)。
    2. Tarjan(离线)算法: 在一次遍历中把所有询问一次性解决,预处理时间复杂度O(nlogn),每次查询时间复杂度O(1),总时间复杂度是O(nlogn+q)。
    3. 倍增算法:利用二分两个节点同时往上走,直到相遇,预处理时间复杂度O(nlogn),每次查询时间复杂度O(logn),总时间复杂度O(nlogn+qlogn)。
    4. RMQ算法:留坑。


    Tarjan(离线)算法



    一.Tarjan算法大致实现过程
    1. 先选择一个节点u为根节点,从根节点开始搜索。(标记u已访问过)
    2. 遍历该点u的所有儿子节点v,并标记v已访问过。
    3. 若v还有儿子节点,对v重复ii操作,否则进入下一操作。
    4. 把v合并到u上(并查集)。
    5. 把当前的点设为u,遍历与u有询问关系的节点v。
    6. 如果v在之前已经被访问过,那么u和v的最近公共祖先就是v通过并查集合并后的父亲节点(注意是合并后),即当前的find(v)。

      二.Tarjan算法的伪代码

      Tarjan(u)           //根节点u
      {
          for each(u,v)
          {
              Tarjan(v);  //v还有儿子节点
              join(u,v);  //把v合并到u上
              vis[v]=1;   //访问标记
          }
          for each(u,v)   //遍历与u有询问关系的节点v
          {
              if(vis[v])
              {
                  ans=find(v);
              }
          }
      }
      


      三. Tarjan算法模拟




      还是原来的图,如图,图中共有8个点,7条边,我们需要寻找最近公共祖先的点对为<3,5>,<5,6>,<2,7>,<6,7>

      先做好初始化工作,开一个pre数组,记录父亲节点,初始化pre[i]=i;
      再开一个vis数组,记录是否已经访问 (memset(vis,0,sizeof(vis)))

      然后开始模拟整个过程

      1.先取1为根节点, 发现其有两个子节点2和3,先搜索2,又发现2有两个子节点4和5,先搜索4,4也有两个子节点6和7,先搜索6,这时发现6没有子节点了,然后寻找与其有询问关系的节点,发现5和7均与6有询问关系,但都没被访问过。所以返回并标记vis[6]=1,pre[6]=4;

      2.接着搜索7,发现7没有子节点,然后寻找与其有询问关系的节点,发现6与其有询问关系,且vis[6]=1,所以LCA(6,7)=find(6)=4。结束并标记vis[7]=1,pre[7]=4;

      3.现在节点4已经搜完,且没有与其有询问关系的节点,vis[4]=1,pre[4]=2;

      4.搜索5,发现其有子节点8,搜索8,发现8没有子节点,然后寻找与其有询问关系的节点,也没有,于是返回,且vis[5]=1,pre[8]=5;

      5.节点5已经搜完,发现有两个与其有询问关系的节点6和7,且vis[6]=1,所以LCA(5,6)=find(6)=2;因为vis[7]=1,所以LCA(5,7)=find(7)=2;遍历完毕返回,标记vis[5]=1,pre[5]=2;

      (find过程:pre[7]=4,pre[4]=2    ==》2 )

      6.节点2已经搜完,发现有一个与其有询问关系的节点7,且vis[7]=1,故LCA(2,7)=find(7)=2。遍历完毕,标记vis[2]=1,pre[2]=1;

      7.接着搜索3,没有子节点,发现有一个与其有询问关系的节点5,因为vis[5]=1,所以LCA(3,5)=find(5)=1;遍历结束,标记vis[3]=1,pre[3]=1;

      (find过程:pre[5]=2,pre[2]=1   ==》1 )

      8.这时返回到了节点1,它没有与之有询问关系的点了,且其pre[1]=1,搜索结束。

      完成求最小公共祖先的操作。



       四.Tarjan 算法的代码

      由于LCA的题目千变万化,下面给出最基础的模板(给出一系列边用邻接表保存,把询问也用邻接表保存,只求LCA,不维护其他值)



      void Tarjan(int now)
      {
          vis[now]=1;
          for(int i=head1[now];i!=-1;i=e1[i].next)
          {
              int t=e1[i].t;
              if(vis[t]==0)
              {
                  Tarjan(t);
                  join(now,t);
              }
          }
          for(int i=head2[now];i!=-1;i=e2[i].next)
          {
              int t=e2[i].t;
              if(vis[t]==1)
              {
                  e2[i].lca=find(t);
                  e2[i^1].lca=e2[i].lca;
              }
          }
      }
      


      倍增算法


      一.倍增算法的前期铺垫

          

      我们记节点v到根的深度为depth(v)。那么如果节点w是节点u和节点v的最近公共祖先的话,让u往上走(depth(u)-depth(w))步,让v往上走(depth(v)-depth(w))步,都将走到节点w。因此,我们首先让u和v中较深的一个往上走|depth(u)-depth(v)|步,再一起一步步往上走,直到走到同一个节点,就可以在O(depth(u)+depth(v))的时间内求出LCA。


      由于节点的最大深度为n,所以这个方法在最坏的情况下一次查询时间复杂度就要O(n),这显然是不够的。于是我们开始考虑优化。


      二.倍增算法的实现过程


      分析刚才的算法,两个节点到达同一节点后,不论怎么向上走,达到的显然还是同一节点。利用这一点,我们就能够利用二分搜索求出到达最近公共祖先的最小步数了。


      首先我们要进行预处理。对于任意的节点,可以通过fa2[v]=fa[fa[v]]得到其向上走2步到达的顶点,再利用这个信息,又可以通过fa4[v]=fa2[fa2[v]]得到其向上走4步所到的顶点。以此类推,我们可以得到其向上走2^k步所到的顶点fa[v][k],预处理的时间点复杂度为O(nlogn)。


      有了k=floor(logn)以内的所有信息后,就可以进行二分所搜的,每次查询的时间复杂度为O(logn)。


      三.倍增算法的代码及简要分析


      void dfs(int u,int pre,int d)  //预处理出每个节点的深度及父亲节点
      {
          fa[u][0]=pre;
          depth[u]=d;
          for(int i=0;i<vec[u].size();i++)
          {
              int v=vec[u][i];
              if(v!=pre)
              {
                  dfs(v,u,d+1);
              }
          }
      }
      
      void init()                    //预处理出每个节点往上走2^k所到的节点,超过根节点记为-1
      {
          dfs(root,-1,0);              //root为根节点
          for(int j=0;(1<<(j+1))<n;j++)   //n为节点数目
          for(int i=0;i<n;i++)
          {
              if(fa[i][j]<0) fa[i][j+1]=-1;
              else fa[i][j+1]=fa[fa[i][j]][j];
          }
      }
      
      int LCA(int u,int v)
      {
          if(depth[u]>depth[v]) swap(u,v);
          int temp=depth[v]-depth[u];
          for(int i=0;(1<<i)<=temp;i++)      //使u,v在同一深度
          {
              if((1<<i)&temp)
                  v=fa[v][i];
          }
          if(v==u) return u;
          for(int i=(int)log2(n*1.0);i>=0;i--)  //两个节点一起往上走
          {
              if(fa[u][i]!=fa[v][i])
              {
                  u=fa[u][i];
                  v=fa[v][i];
              }
          }
          return fa[u][0];
      }



      基于RMQ的LCA算法

      一、主要思路


      大家都知道DFS序吧(不知道的可以先自行百度),对一棵树可以一遍DFS处理出搜索过程中进入某个点以及走出某个点的编号,ss[i]和tt[i].同时顺便得到每个点距离根节点的距离depth[i]。


      那么我们回归到LCA的定义,假设我们要求a点和b点的LCA,我们要找的便是从a点走到b点过程中离根节点最近的那个点i,即i满足min(ss[a],ss[b])<=ss[i]<=max(ss[a],ss[b])且depth[i]最大的i。而这可以利用RMQ高效求得。


      二.RMQ算法的代码及简要分析



      int id[2*maxn];        //保存DFS时每个编号对应的节点编号
      int depth[2*maxn];     //保存DFS时每个编号对应的节点深度
      int ss[maxn];          //保存每个节点在DFS时第一次出现的编号
      int dp[2*maxn][30];    //ST预处理时的数组,用以查询区间里深度最小的编号(DFS序编号)
      
      void dfs(int u,int pre,int dep)
      {
          id[++tot]=u;
          ss[u]=tot;
          depth[tot]=dep;
          for(int i=head[u];~i;i=e[i].next)
          {
              int v=e[i].v;
              if(v==pre) continue;
              dfs(v,u,dep+1);
              id[++tot]=u;
              depth[tot]=dep;
          }
      }
      
      void ST(int n)      //n一般取2*n-1
      {
          int k=(int)(log2(1.0*n));
          for(int i=1;i<=n;i++) dp[i][0]=i;
          for(int j=1;j<=k;j++)
          for(int i=1;i+(1<<j)-1<=n;i++)
          {
              int a=dp[i][j-1];
              int b=dp[i+(1<<(j-1))][j-1];
              if(depth[a]<depth[b]) dp[i][j]=a;
              else dp[i][j]=b;
          }
      }
      
      int RMQ(int l,int r)
      {
          int k=(int)(log2(1.0*r-l+1));
          int a=dp[l][k];
          int b=dp[r-(1<<k)+1][k];
          if(depth[a]<depth[b]) return a;
          else return b;
      }
      
      int LCA(int x,int y)
      {
          int l=ss[x];
          int r=ss[y];
          if(l>r) swap(l,r);
          return id[RMQ(l,r)];
      }
      


      相较而言,好像RMQ的效率略高一些。






      相关题目链接

      1.POJ   1330 Nearest Common Ancestors (Tarjan + 倍增 + RMQ) 模板的运用看这里
      2.POJ   1986 Distance Queries
      3.HDOJ  2586 How far away?
      4.POJ   3728 The merchant



      【更新】

      2017/7/30 增加LCA倍增算法,并修改原有错误

      2017/9/13 增加RMQ算法。




      • 29
        点赞
      • 80
        收藏
        觉得还不错? 一键收藏
      • 7
        评论

      “相关推荐”对你有帮助么?

      • 非常没帮助
      • 没帮助
      • 一般
      • 有帮助
      • 非常有帮助
      提交
      评论 7
      添加红包

      请填写红包祝福语或标题

      红包个数最小为10个

      红包金额最低5元

      当前余额3.43前往充值 >
      需支付:10.00
      成就一亿技术人!
      领取后你会自动成为博主和红包主的粉丝 规则
      hope_wisdom
      发出的红包
      实付
      使用余额支付
      点击重新获取
      扫码支付
      钱包余额 0

      抵扣说明:

      1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
      2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

      余额充值