LCA 最近公共祖先

首先,介绍何谓最近公共祖先,其实就是对于一颗二叉或者多叉树来说,每个节点都有祖先节点(根节点除外),对于任意两个点,ab,它们可能有多个公共的祖先点c,即ca的祖先且cb的祖先,我们定义深度最大的那个公共祖先Cab的最近公共祖先,这个点是唯一的。 

对于求最近公共祖先的算法有不少,著名的是LCA的在线算法和离线算法,看了这么多网上的代码,很少能找到我中意的,而且有些在线算法时间复杂度实在太高,因此我觉得有必要把自己想的一些东西拿出来给大家分享分享。 

我们想想普通的方法来求ab两个点的最近公共祖先C

int LCA(int a,int b)//求a,b的LCA
{
    while(a!=b)//找到a,b的LCA
    {
        if(p[a].deep>p[b].deep)
        {
            a=p[a].father;
        }
        else
        {
            b=p[b].father;
        }
    }
    return a;
}

意思就是一步一步向上寻找a,b点的父节点,知道找到他们的父节点相同,那么此时的点就是点C;但是如果这棵树很大,那么这个算法实在太慢了,因此我们就优化这个地方。 

我们来看看这样一棵树:

 

    0              -------deep=0

 

         /  \

 

       1     2          -------deep=1

 

     /   \

 

   3      4              -------deep=2

 

         /    \

 

       5       6          ------deep=3

 

                   \

 

                      7     -------deep=4

 

我们以深度为界限把数分段,比如我把深度为0和1的点作为第一段,深度2和3的点作为第二段,然后查看a和b是否在同一段,若不是同一段,则向上一段去寻找,直到a和b为同一段,然后再依照父节点去寻找最近公共祖先C:

int LCA(int a,int b)//求a,b的LCA
{
    while(p[a].sec!=p[b].sec)//找到a,b亮点所在的段
    {
        if(p[a].deep>p[b].deep)
        {
            a=p[a].sec;
        }
        else
        {
            b=p[b].sec;
        }
    }
    while(a!=b)//找到a,b的LCA
    {
        if(p[a].deep>p[b].deep)
        {
            a=p[a].father;
        }
        else
        {
            b=p[b].father;
        }
    }
    return a;
}

代码中p[u].sec为点u所归属的段,也就是归属的集合,也就是我们常说的并查集来分段。具体怎么分,方法有很多,可以以一个分叉点到下一个分叉点之间的所有点作为一个集合,而且网上大多数算法都是这么做的,这样做起来最好的时间复杂度是o(lgn),但是最复杂的情况将是o(n)。在这里我介绍一种特殊的分段方法,最好和最复杂的时间复杂度都是o(sqrt(n)),即依照节点的深度deep来分段,每一段的长度为sqrt(max(deep)),比如一棵树最大深度为100,那么深度为1~9的点为集合0,深度10~19的点为集合1,依次类推。 

补充: 

当然如果我们这样来分段,在下面的代码中: 

p[u].deep%sec==0;时,我们应该把p[u].sec=p[p[u].father].sec+1;但是这样子的复杂度还是有点高,为了让复杂度再降低,我们把段分的更多,也就是当p[u].deep>=sqrt(max(deep))的时候,我们把段标记为它的父节点,这样深度相同的节点也可以在不同的段里面,可以让我们更快的查询最近公共祖先。(这里感谢一楼的提醒,一开始忘记说了)

void setsection(int u,int sec)//构建点u属于的段集合,每一段深度为sec
{
    if(p[u].deep<sec)
    {
        p[u].sec=0;
    }
    else
    {
        if(p[u].deep%sec==0)
        {
            p[u].sec=p[u].father;
        }
        else
        {
            p[u].sec=p[p[u].father].sec;
        }
    }
    for(unsigned i=0;i<p[u].next.size();i++)
    {
        int k=p[u].next[i];
        if(p[k].father==u)
        {
            setsection(k,sec);
        }
    }
}

说到这里,整个LCA的算法也就差不多讲完了,并查集分段的方法无外乎这两种,我的代码是后者,毕竟还是比较好理解的,如果不懂也可以留言,下面我把全部的代码都贴上来。

 

我们一开始给的是一个图,但是我们要把这个图变为一棵树,以任意节点作为根节点建树(此处以点0为root),建树过程标记父节点和深度,然后给节点分段,最后可以做任意次数的查询:

 

步骤:1.为图建树,标记节点的父节点和深度

 

   2.为树上的节点分段,使用并查集

 

       3.直接查询(a,b)两个节点的LCA即可。

 //====================================================================
 //Name       :LCA最近公共祖先
 //Author     :hxf
 //copyright  :http://www.cnblogs.com/Free-rein/
 //Description:
 //Data       :2012.8.20
 //========================================================================
 #include<iostream>
 #include<algorithm>
 #include<stdio.h>
 #include<math.h>
 #include<string>
 #include<cstring>
 #include<vector>
 #include<stack>
 #include<queue>
 #define MAXN 1040
 #define inf 10100
 #define pi 3.141592653589793239
 using namespace std;
 struct Tree{
     vector<int> next;//子节点
     int father;//父节点
     int deep;//深度
     int sec;//属于的段
 }p[50050];
 int visit[50050];
 int maxdeep;//最大深度
 void dfs(int u)//构建多叉树,找到父节点和深度
 {
     visit[u]=1;
     for(unsigned i=0;i<p[u].next.size();i++)
     {
         int k=p[u].next[i];
         if(visit[k]==0)
         {
             p[k].deep=p[u].deep+1;
             p[k].father=u;
             dfs(k);
         }
     }
     maxdeep=max(maxdeep,p[u].deep);
 }
 void setsection(int u,int sec)//构建点u属于的段集合,每一段深度为sec
 {
     if(p[u].deep<sec)
     {
         p[u].sec=0;
     }
     else
     {
         if(p[u].deep%sec==0)
         {
             p[u].sec=p[u].father;
         }
         else
         {
             p[u].sec=p[p[u].father].sec;
         }
     }
     for(unsigned i=0;i<p[u].next.size();i++)
     {
         int k=p[u].next[i];
         if(p[k].father==u)
         {
             setsection(k,sec);
         }
     }
 }
 void preLCA()
 {
     maxdeep=0;
     dfs(0);
     setsection(0,(int)sqrt(maxdeep));//每一段深度为sqrt(maxdeep)
 }
 int LCA(int a,int b)//求a,b的LCA
 {
     while(p[a].sec!=p[b].sec)//找到a,b亮点所在的段
     {
         if(p[a].deep>p[b].deep)
         {
             a=p[a].sec;
         }
         else
         {
             b=p[b].sec;
         }
     }
     while(a!=b)//找到a,b的LCA
     {
         if(p[a].deep>p[b].deep)
         {
             a=p[a].father;
         }
         else
         {
             b=p[b].father;
         }
     }
     return a;
 }
 int main()
 {
     int n;//n个节点
     while(scanf("%d",&n)!=EOF)
     {
         for(int i=0;i<n;i++)
         {
             p[i].next.clear();
             p[i].father=0;
             p[i].deep=0;
             p[i].sec=0;
         }
         for(int i=0;i<n-1;i++)
         {
             int x,y;
             scanf("%d %d",&x,&y);
             p[x].next.push_back(y);
             p[y].next.push_back(x);//建边
         }
         memset(visit,0,sizeof(visit));
         preLCA();//建树
         ///
         int m;//做m次查询
         scanf("%d",&m);
         while(m--)
         {
             int a,b;
             scanf("%d %d",&a,&b);
             int lcaab=LCA(a,b);//得到a,b的LCA
             printf("%d\n",lcaab);
         }
     }
     return 0;
 }


点击 查看原文


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值