Problem7747--学习系列 —— 图论 —— 最近公共祖先 —— 倍增求 LCA

7747: 学习系列 —— 图论 —— 最近公共祖先 —— 倍增求 LCA

[Creator : ]
Time Limit : 1.000 sec  Memory Limit : 256 MiB

Description

【概念】

【最近公共祖先】

Lowest Common Ancestors (LCA) 。是指在有根树中,找出某两个结点 $u$ 和 $v$ 最近的公共祖先。
在一棵没有环的树上,每个节点肯定有其父亲节点和祖先节点,而最近公共祖先,就是两个节点在这棵树上深度最大的公共的祖先节点。
换句话说,就是两个点在这棵树上距离最近的公共祖先节点。

如上图所示 4 和 5 的最近公共祖先是 2,5 和 3 的最近公共祖先是 1,2 和 1 的最近公共祖先是 1。

【问题】




如上图的关系中,求 LCA。


【暴力算法】

以 $17$ 和 $18$ 为例,既然要求 LCA,那么我们就让他们一个一个向上爬,直到相遇为止。第一次相遇即是他们的LCA。
模拟一下就是:
17->14->10->7->3
18->16->12->8->5->3
最终结果就是 $3$。
当然我们知道,这样的算法最终得到一个 TLE。


【倍增】

所谓倍增,就是按 $2$ 的倍数来增大,也就是跳 1,2,4,8,16,32……。
不过在这我们不是按从小到大跳,而是从大向小跳,即按 ……32,16,8,4,2,1 来跳,如果大的跳不过去,再把它调小。这是因为从小开始跳,可能会出现“悔棋”的现象。拿 5 为例,从小向大跳,5≠1+2+4,所以我们还要回溯一步,然后才能得出5=1+4;而从大向小跳,直接可以得出5=4+1。
这也可以拿二进制为例,5(101),从高位向低位填很简单,如果填了这位之后比原数大了,那我就不填,这个过程是很好操作的。
同样这里以编号为 $17$ 和 $18$ 结点为例
17−>3
18−>5−>3
可以看出向上跳的次数大大减小。这个算法的时间复杂度为 $O(mlogn)$,已经可以满足大部分的需求。
【时间复杂度】
$O(mlogn)$,其中 $m$ 表示查询次数,$n$ 表示顶点数量。

Input

【fa 数组】


【倍增求 LCA】

【Step 1】

将 $v, w$ 跳到同一层。

如上图,deep[v]=10, deep[w]=1,所以我们知道两者深度差为 $10-1=9$,$9$ 对应的二进制为 $1001$,也就是说,先跳一次 $8$。

然后我们再跳 $1$ 步,就将 $v,w$ 变成同一层。


【Step 2】 

将 $v,w$ 一起跳到 LCA 的下一层。
现在,我们系统的结构如下图。

如上图所示。deep[v]=11,LCA 的下一层的 deep 为 $4$,因此 $11-4=7$,对应的二进制为 $111$。
第一次跳 $4$ 层。如下图。

第二次跳 $2$ 层,如下图。

第三次跳 $1$ 层,如下图。

这样,$v,w$ 的最近公共祖先就应该是 fa[v][0],即然 $v$ 再跳 $2^0=1$ 步即可。

Output

【倍增求 LCA】

倍增求 LCA 是一种在线算法。

想要实现这个算法,首先我们要记录各个点的深度和他们 $2^i$ 级的的祖先,用数组 deep 表示每个节点的深度,fa[i][j] 表示节点 $i$ 的 $2^j$ 级祖先。

【数据保存】

使用数组。和图论使用邻接表存图的方法一样。

//存图
const int M=2e6+10;
LL e[M], ne[M], idx;
//LCA数据 
const int N=5e5+10;
LL h[N];//头节点 
LL deep[N];//编号为i的节点深度 
LL dis[N];//编号为i的节点到跟节点距离 
LL fa[N][30+2];//节点i的2^j级祖先 

【预处理】

BFS
void bfs(LL st) {
	queue<LL> que;
	que.push(st);
	deep[st]=1;
	
	while (que.size()) {
		LL u=q.front();
		q.pop();
		
		for(int i=h; i!=-1; i=ne[i]) {
			LL v=e[i];
			if (deep[v]) {
				continue;
			}
			
			deep[v]=deep[v]+1;
			fa[v][0]=u;
			
			for(LL j=1; j<=30;j++) {
				f[v][j]=f[f[v][j-1]][j-1];
			}
			q.push(v);
		 } 
	}
}


DFS
//u表示当前节点,
//fath表示它的父亲节点
void dfs(LL now, LL fath) {
	fa[0]=fath;//更新当前x的爸爸,即2^0=1代祖宗
	deep = deep[fath]+1;//孩子是爹的深度+1
	for (LL i=1; (1<<i)<=deep; i++) {
		fa[i]=fa[fa[i-1]][i-1];//这个转移可以说是算法的核心之一
						//意思是f的2^i祖先等于f的2^(i-1)祖先的2^(i-1)祖先
						//2^i=2^(i-1)+2^(i-1)
	}
	for(int i=h; i!=-1; i=ne[i]) {
		LL v=e[i];
		if (v!=fath) {
			dfs(v,u);
		}
	}
}



【求 LCA】

LL lca(LL x,LL y){
	if (deep[x]<deep[y]) {
		swap(x,y);//令x为深度较深的结点
	}
	for (LL i=30; i>=0; i--) {
		if(dep[fa[x][i]]>=dep[y]) {
			x=fa[x][i];//若x的2^i级爸爸的深度大于等于y的深度,更新x
		}
	}
	if (x==y) {
		return x;//特判,x的爸爸就是y
	}
	for (LL i=30; i>=0; i--) {
		if (fa[x][i]!=fa[y][i]) {
			x=fa[x][i];
			y=fa[y][i];//若x和y的爸爸不相同,同时往上跳直到找到x和y的共同爸爸
		}
	}
	return fa[x][0];//返回x的爸爸
}

Source/Category