首先,介绍何谓“最近公共祖先”,其实就是对于一颗二叉或者多叉树来说,每个节点都有祖先节点(根节点除外),对于任意两个点,a,b,它们可能有多个公共的祖先点c,即c为a的祖先且c为b的祖先,我们定义深度最大的那个公共祖先C为a,b的最近公共祖先,这个点是唯一的。
对于求最近公共祖先的算法有不少,著名的是LCA的在线算法和离线算法,看了这么多网上的代码,很少能找到我中意的,而且有些在线算法时间复杂度实在太高,因此我觉得有必要把自己想的一些东西拿出来给大家分享分享。
我们想想普通的方法来求a,b两个点的最近公共祖先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;
}
点击 查看原文