最近公共祖先详解

7 篇文章 0 订阅
2 篇文章 0 订阅

(原标题:最近公共祖先学习笔记)
其实是假的。
这个,老早就学过了,但是显然由于当时理解得不透彻,很快就遗忘了。
今天,做了一份模拟试题,第一题就是LCA,汗!没办法,只好温习,终于理解透彻了。
最近公共祖先,顾名思义,就是指求树上两个点的第一个公共祖先,或者说是深度最大的公共祖先。
求这个有什么用呢?
一个常见的用途就是求两点简单路径。
有人可能说,这不简单吗?我们把两个点调整到同一个高度,然后一个一个往上找,不就好了?
简单的树是可以,但是对于一棵庞大的树呢?事实上,我们碰到的求LCA大多是很庞大的树,所以我们应该怎么办呢?
所以我们就要在刚刚的思路上做一个优化
我们来想一想,为什么刚刚的算法这么慢?
因为我们所求的两个点和他们的LCA可能相差甚远,而我们却必须要一层一层去找,有没有什么方法可以跳过这么一段呢?
至此,我们可能会想起倍增,相同的操作,满足结合律,用倍增再合适不过了。
所以我们就可以考虑倍增来处理。
首先,我们需要用bfs来建树,在建树的过程中,我们至少需要初始化各个节点的深度,其他的附加数据则由题目决定。
在继续之前,我们先介绍一个神奇的数组。

神奇的数组:f[k][u],它表示什么意思呢?它存储的是节点u的2k级祖先

所以我们在建树结束时,就需要初始化该数组,初始化每个节点的1级祖先,也就是20级祖先。
是不是已经有了倍增的味道?
初始化结束后,就是lca的核心算法。
我们先读入两个点,然后依据之前的思想,我们需要把两个点调整到一个深度上。
我们先忽略这个算法(为了保持核心算法的连贯性),假设两个点已经在同一深度上,我们就可以从大到小,依次枚举两者的2k级祖先,如果不相等,说明二者的LCA在当前深度之上,所以我们可以将两个点变成当前遍历到的节点,继续刚刚的过程,直到结束。
最后,判断所得是否相等,不相等就取他们的父节点,相等就直接输出就好了。
讲得比较匆忙(毕竟还有3天就NOIP了),所以自己结合代码理解理解吧(明年会重新解释):


如前所述,我回来解释了。(但好像不止一年了)
之前的解释过于疏漏(其实就是没解释),我们接着进一步解释二者向上搜索的过程。
首先有一点是显然的,那就是二者的最近公共祖先的所有祖先,均是二者的公共祖先,也就是说如果两者的k级祖先相等,那么二者的k+1、k+2,……级祖先均相等,并一直延伸至根节点。
所以,我们按照k从大到小的顺序枚举二者2k级祖先,如果相等,基于上述,说明二者的最近公共祖先要么是这个节点,要么是这个节点的子节点,那就继续缩小k进行搜索。如果发现二者不相等了,那就说明二者最近公共祖先是当前搜索的两个祖先的祖辈节点,所以我们就将搜索的两个点调节成当前找到的两个节点,然后继续缩小k搜索。
为什么?为什么我们可以不改变k搜索?这一点很容易理解。假设我们当前的搜索深度为2k,也就是说2k深度处没有二者的公共祖先,而2(k-1)深度处是二者的祖先(如果不是,我们就会更早的执行这一步了),所以二者的最近公共祖先就锁定在深度为[2(k-1),2k)的区间上,而这个区间的长度为2k-2(k-1)=2(k-1)因为此时我们已经调整搜索的两个点为当前2k深度的节点,所以搜索他们的最近公共祖先只有从(k-1)开始搜索就行了,而我们下一步要搜索的正好是(k-1),所以就不必费心的去修改k了。
这样一直进行下去,最后判断二者是否相等,如果相等就返回自身,如果不相等就返回他们的父节点。
这最后一步返回的判断,是为了防止特殊情况的发生,即两节点本身就具有祖辈关系(再特殊一点就是两个节点为同一节点),这种情况下,调整完深度后,两者即为同一节点,也就是我们要的答案,也正因为二者的相等关系,所以接下来的搜索过程中不会有任何变化,最后如果返回二者的父节点,就会造成答案错误,所以要加以判断。正常的其他情况均要返回二者的父节点。
这就是核心算法的思想,接下来是填坑时间。我们先来说说我们是怎么调整深度的。
我们先想想暴力调整深度是怎么调整的,不停地向上回溯,寻找父节点,直到深度相等。怎么?这个步骤是不是和我们上面寻找地过程有些许类似?都是要向上寻找特定的父节点。所以我们要充分利用我们处理出的特殊函数来调整。
利用的核心思想就是二进制,即十进制的数均可表示成若干个不重复的2的次幂的和,所以我们只要把需要调整的深度差分解成二进制,然后利用我们的函数去跳即可。
最后是我们如何处理出这个关键的函数。这一点就很简单了,首先在建树时,我们就要赋初值,即每个节点的一级祖先。然后我们只需要对k遍历,然后再遍历每个节点即可,这一点就比较好理解了,即一个节点的2(k+1)级祖先就是他的2k级祖先的2k级祖先,这样我们就可以把这个函数构造好了。
这回解释的应该算较为详细了,代码放在下面,大家可以结合着看:

//最近公共祖先(LCA)
//考虑预处理出f[logN][N],其中f[k][u]表示u的2^k级祖先(1级祖先为u的父亲,2级祖先为u的父亲的父亲,以此类推)。
//求u,v的LCA时,先将u,v调整到同一深度,然后进行一个类似二分的过程。
//初始化前,需要保存自己的一级祖先
//用bfs初始化一级祖先,同时记录深度 
void bfs(){
    queue<int> p;
    p.push(1);
    vis[1]=1;
    deep[1]=1;
    while (!p.empty()){
        int cur=p.front();
        p.pop();
        for(int i=0;i<G[cur].size();i++){
            if(vis[G[cur][i]]==0){
                p.push(G[cur][i]);
                vis[G[cur][i]]=1;
                f[0][G[cur][i]]=cur;
//                g[0][G[cur][i]]=H[cur][i];
                deep[G[cur][i]]=deep[cur]+1;
            }
        }
    }
} 
void init(){
    for (int k=0;k<19;k++){
        for (int j=1;j<=n;j++){
            if (f[k][j]==-1){
            	f[k+1][j]=-1;
			}else{
                f[k+1][j]=f[k][f[k][j]];
            }
        }
    }
} 
int jump(int u,int step){
	for(int k=0;k<=20;k++){
		if((step&(1<<k))>0){
			u=f[u][k];
		}
	}
	return u;
}

int qlca(int u,int v){
	if(deep[u]<deep[v]){
		swap(u,v);
	}
	u=jump(u,deep[u]-deep[v]);
	for(int k=20;k>=0;k--){
		if(f[u][k]!=f[v][k]){
			u=f[u][k];
			v=f[v][k];
		}
	}
	return u==v?u:fa[u];
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值