LCA问题(least Common Ancestors,最近公共祖先问题),是指给定一棵有根树T,给出若干个查询LCA(u,v)(通常查询数量较大),每次求树T中两个顶点u和v的最近公共祖先,即找到一个节点,同时是u和v的祖先,并且深度尽可能的大(尽可能远离树根).
LCA问题的解法有很多,有一般解法,基于tarjan的解法,跳表解法以及RMQ和LCA互相转化的解法。下面我们依次介绍一下:
(一):一般解法
根据树的结构,树中除根节点外的每个节点有且只有一个父节点,所以我们可以记录好每一个节点的父节点,这样我们能够根据父节点的父节点,一次来遍历到每个节点的所有祖先节点(祖先节点就是节点的父节点,节点父节点的父节点,依次类推)。
然后我们要查询两个节点的最近公共祖先,只需要找到两个节点往上找时,第一个相同的祖先。
为每个节点标记好它的父节点只需要维持一个数组_father[n],然后在深度遍历的时候存在来就好。
为了方便找到第一个祖先,我们可以维持一个数组depth[n],因为它俩的祖先一定是深度相同的节点(同一个节点嘛,所以深度肯定相同),所以我们可以先将深度较大的节点u向上查找,找到它的某个祖先s,使得这个s节点的深度和另外一个节点v的深度一样,然后判断s和v是否相等,如果不相等就俩者同时向上查找祖先,这样能够保证俩这的深度一样,直到俩个节点是同一个节点,就说明找到了共同的祖先。
C++代码实现如下:
int flag[105]; //深度遍历时标记节点是否访问过
int _father[105]; //标记每个节点的父节点
int depth[105]; //标记每个节点的深度
/*
ntree:树的表示,二维数组
start:深度遍历的起始节点
l:树中节点的个数
deep:当前遍历的节点在树中的深度
*/
void dfs(int** ntree,int start,int l,int deep){
flag[start]=1; //标记为访问过
depth[start]=deep; //标记好深度
for(int i=0;i<l;i++){ //先到达一定的深度再说
if(ntree[start][i]&&flag[i]==0){
_father[i]=start; //标记好关系
dfs(ntree,i,l,deep+1);
}
}
return;
}
int getSplitNode(int** matrix,int l, int indexA, int indexB) {
memset(flag,0,sizeof(flag));
memset(_father,0,sizeof(_father));
_father[0]=0;
int i,j;
dfs(matrix,0,l,0); //调用dfs进行处理
while(depth[indexA]>depth[indexB]) //如果indexA的深度更大,则向上找祖先,直到找到的祖先深度和节点B一样
indexA=_father[indexA];
while(depth[indexB]>depth[indexA]) //和上面一样,只是情况不同
<span style="white-space:pre"> </span>indexB=_father[indexB];
while(indexA!=indexB){ //在保证深度相同的情况下,往上查找,直到俩者是同一节点
indexA=_father[indexA];
indexB=_father[indexB];
}
return indexA; //返回最近祖先节点
}
(二):基于tarjan算法的解法
其实这个解法与tarjan算法的联系不是很大,只是有点相似而已,并没有用到tarjan算法。当我们深度遍历一棵树时,我们选择后序遍历它,即左右根的形式来遍历一棵树。假设我们当前需要查询节点u和节点v的公共祖先,我们在后序遍历的过程中,假设现在我们已经访问到节点u,对于节点v只有两种形式,一是被访问过,二是还没被访问。
如果节点v已经被访问过,则根据后序遍历的特点(左右根),节点u和v的最近公共祖先一定是在由v所在的集合S和节点v这个集合W(这个集合中只要u)的公共祖先。而我们根据后序遍历左右根的特点,以上两个集合S和W的祖先一定是集合W的祖先。根据后序左右根特点,假设v是右子树,u是根,那么两个集合的祖先显然集合S的祖先就是u(根);假设u是在右子树中,v是在左子树中,因为左右子树的最近公共祖先就是根,而根又是左子树集合的公共祖先,所以两个集合的祖先还是集合的祖先。假设v是左子树,u是根,则公共祖先还是根,也就是v所在集合的祖先(以上的左子树,根,右子树都是相对的,即相对于节点v所在的集合S来看的,这样做的目的就是直观阐述,当节点v被访问过的时候,节点u和节点v的最近祖先就是节点v所在集合S的祖先).
举个例子,如下图:当u=1,v=4时,在后序遍历的过程中,访问u时,v已经被访问过了,已访问节点集合{4,7,5}的祖先节点就是1,俩者相等,所以就是集合{4,7,5}的祖先;再当u=7,v=4时,访问7时4已经被访问,而7相对于已访问集合{4}是右子树,所以二者的祖先根就是集合{4}的祖先,也就是1.总之不管哪种情况,都是已访问几点集合的祖先。
如果节点v没有被访问过,那我们就不用做处理,等到下次访问到节点v时,节点u已经被处理了,按上面的方式进行理。
在实际实现的过程中,我们需要记录集合的祖先。对于集合,我们可以用并查集来实现,对于祖先,我们可以维持一个数组ancestor,来记录每个节点的祖先节点。当我们要查询一个集合的祖先节点时,只需要查询这个集合的代表元素r的ancestor值。比如,我们要查询节点4所在的集合的祖先节点时,只需要先找到4所在集合的代表r,然后找到ancstor[r]的值就是这个集合的祖先值。
C++代码实现如下:
//先实现并查集
int n;
int father[105]; //每个节点的父节点
int rnk[105]; //树中节点的个数
int ancestor[105]; //已访问节点集合的祖先
void initSet(){
for(int i=0;i<n;i++){
father[i]=i; //初始化时,所在子树的祖先就是自己
rnk[i]=1; //所在树的深度为0
}
}
int findSet(int x){
if(x!=father[x])
father[x]=findSet(father[x]); //压缩式的查找,在查找过程中更新每个节点的祖先
return father[x];
}
void unionSet(int x,int y){ //合并子树,把节点数小的树合并到节点数大的树
x=findSet(x);
y=findSet(y);
if(x==y)
return;
if(rnk[x]>=rnk[y]){
father[y]=x;
rnk[x]+=rnk[y];
}
else{
father[x]=y;
rnk[y]+=rnk[x];
}
}
int flag[105]; //记录点是否为访问过
vector<int> tree[105]; //树的表示
vector<int> query[105]; //查询的表示
void tarjan(int u){ //访问到集合u时
for(int i=0;i<tree[u].size();i++){
int v=tree[u][i]; //假设这个节点是v
tarjan(v);
unionSet(u,v); //将子节点和根节点合并,并查集的作用只是代表一个集合,仅仅当做一个集合使用
ancestor[findSet(u)]=u; //合并后的集合的祖先为u,只要标记这个集合的代表元素的祖先为x就行,这个集合
//内的其他元素能够通过findSet来找到代表,再利用代表找到祖先
}
flag[u]=1;
for(i=0;i<query[u].size();i++){
if(flag[query[u][i]]) //如果另外一个节点已经被访问过,则输出另外一个节点所在集合的祖先
cout<<u<<"和"<<query[u][i]<<"的最近公共祖先为:"<<ancestor[findSet(query[u][i])]<<endl; //找到节点query[u][i]所在集合的代表的祖先,
<span style="white-space:pre"> </span>//也就是这个集合的祖先,只是用代表的ancestor值标记一下而已
}
}
(三):跳表解法
跳表解法和普通解法其实很像,对于节点u和节点v,也是先通过father数组让u和v的深度变为一样,然后再向上跳,直到向上取到的节点值一样(即找到根节点).但是它跳的速度更快,一般解法跳需要的时间为o(n)(因为普通解法是一个一个往上跳);而跳表解法跳的时间复杂度是o(logn),因为它跳的时候是按2的次幂跳,下面详细为大家解答。
首先,我们需要记录好每一个节点的深度(根节点深度为0),然后我们再从任意两个节点的最近祖先必定深度是一样(同一个点,深度肯定一样)的这个点入手。
假设,我们要查询的两个节点是节点u和节点v,其中u的深度为p,v的深度为q。首先,我们将二者的中较深的进行调整,向上跳,跳到同一层。然后二者同时向上跳,直到跳到同一层为止。但是这个跳的方法和以往的不一样,它是以按2的次幂的形式跳,也就是跳2^0,2^1,2^2等等层。因为对于任意一个整数n,它都可以找到唯一的一组x1,x2,x3,.....
来满足一下等式:
n=2^x1+2^x2+2^x3+......
所以对于任意一个整数,都能通过按2的次幂跳来实现,这个时候规定从高次幂到低次幂的跳。举个例子,对于6,我们知道它等于6=2^1+2^2,当跳的时候,是先跳2^2,再跳2^1。另一方面,我们知道每一个节点的层数,所以我们就能通过logn来求出那个2的次幂的上限。但我们可能会考虑到一个问题,那就是如果当节点u和节点v已经再同一层的时候,可能它们先跳一次高次幂的层数时就已经找到了公共祖先,但这个却不是最近的公共祖先。对于这个问题我们这样解决,我们不去找公共祖先,而是去找最近公共祖先下一层的那个节点,也就是最近公共祖先的子节点,这样就能避免一次跳过头,通过判断条件就能实现。
为了实现在跳了2的多少次幂后能够很快的定位到节点,我们为每个节点维持一个数组anc[i][j],表示节点i跳了2^j次幂层之后到达的节点。如下图,anc[7][1]就是节点1
对于anc[i][j]数组的初始化问题:
1.当j=0时,显然anc[i][j]就是节点i的父节点,就像普通解法的father数组意义一样
2.当j>0时,我们不能直接求出跳多层的值,然后我们可以转化,因为anc[i][j]=i+2^j=i+2^(j-1)+2^(j-1)=
(i+2^(j-1))+2^(j-1)=anc[anc[i][j-1]][j-1],所以就能这样地退出来。也就是anc[i][j]=anc[anc[i][j-1]][j-1]
所以具体实现代码如下:
#include<iostream>
#include<vector>
#include"string.h"
#include<math.h>
using namespace std;
int n; //树中的节点
vector<int> tree[105];
int anc[105][7]; //支持跳表的数组
int depth[105]; //记录好每一个节点在树中的深度
void inputTree() //输入树
{
cin>>n; //树的顶点数
for(int i=0;i<n;i++) //初始化树,顶点编号从0开始
tree[i].clear();
for(i=1;i<n;i++) //输入n-1条树边
{
int x, y;
cin>>x>>y; //x->y有一条边
tree[x].push_back(y);
}
}
//深度遍历,为每个节点计算深度和其父节点
void dfs(int u,int deep){ //初始化,利用深度遍历
depth[u]=deep; //标记好深度
for(int i=0;i<tree[u].size();i++){
anc[tree[u][i]][0]=u; //标记好父节点
dfs(tree[u][i],deep+1);
}
return;
}
//计算anc[][]数组的函数
void calAnc(){ //然后就是计算各种anc的关系
int max=int(log(n)/log(2)); //最大的可能层数
for(int j=1;j<=max;j++){ //根据anc[i][j]=anc[anc[i][j-1]][j-1]可知是渐进的,所以得一层一层来
for(int i=0;i<n;i++){ //对于每一个节点,都求
if(anc[i][j-1]!=-1){ //即如果没有越界
anc[i][j]=anc[anc[i][j-1]][j-1];//所以这个数组初始化是得全部赋值为-1
}
}
}
}
//初始化函数
void init(){
memset(anc,-1,sizeof(anc));
dfs(0,0);
calAnc();
}
//求2^i的函数
int getTwo(int i){
int res=1;
while(i>0){
res*=2;
i--;
}
return res;
}
//查询函数
int query(int u,int v){
if(depth[u]<depth[v]){ //令u为较深的,然后u向上调整到同一层
int tmp=u;
u=v;
v=tmp;
}
int i;
int logs=int(log(depth[u])/log(2));
//将u和v跳到同一层
for(i=logs;i>=0;i--){
if(depth[u]-getTwo(i)>=depth[v])
u=anc[u][i];
}
if(u==v) //相等则直接返回
return u;
//公共跳到最近公共祖先的下面
for(i=logs;i>=0;i--){ //向上找,找到最后一个u!=v的,这时的u和v,就是原始的u和v的最近祖先的子节点
if(anc[u][i]!=-1&&anc[u][i]!=anc[v][i]){
u=anc[u][i];
v=anc[v][i];
}
}
return anc[u][0]; //则此时u一个是最近公共节点的子节点
}
int main()
{
inputTree(); //输入树
init(); //初始化
int m; scanf("%d", &m); //查询个数
while (m--)
{
int u, v; scanf("%d%d", &u, &v);//查询u和v的LCA
printf("%d和%d的最近公共祖先为:%d\n", u, v, query(u, v));
}
return 0;
}
(四):由于关于RMQ的解法过于复杂,将在以后为大家解答。