倍增做法请见《题解:P3379 【模板】最近公共祖先(LCA)倍增》。

树剖求最近公共祖先

一棵有根树 T T T 的两个结点 u u u v v v 的最近公共祖先表示一个结点 x x x,满足 x x x u u u v v v 的祖先且 x x x 的深度尽可能大。本题解讲解用重链剖分求两节点的最近公共祖先。

暴力

首先计算出结点 a a a b b b 的深度 d 1 d1 d1 d 2 d2 d2。如果 d 1 > d 2 d1>d2 d1>d2,将 a a a 结点向上移动 d 1 − d 2 d1-d2 d1d2 步,如果 d 1 < d 2 d1<d2 d1<d2,将 b b b 结点向上移动 d 2 − d 1 d2-d1 d2d1 步,现在 a a a 结点和 b b b 结点在同一个深度了。下面只需要同时将 a a a b b b 结点向上移动,直到它们相遇(到达同一个结点)为止,相遇的结点即为 a a a b b b 结点的最小公共祖先。

该算法时间复杂度为 O ( n m ) O(nm) O(nm),对于多次询问的题目(比如本题)不能解决。

思想

树链剖分用于将树分割成若干条链的形式,以维护树上路径的信息。具体来说,将整棵树剖分为若干条链,使它组合成线性结构,然后用其他的数据结构维护信息,但本题目的是求最近公共祖先,不需要如此麻烦,就不展开讲述。

重链剖分将树上的任意一条路径划分成不超过 O ( log ⁡ n ) O(\log n) O(logn) 条连续的链,每条链上的点深度互不相同,即是自底向上的一条链,链上所有点的最近公共祖先为链的一个端点

为方便表示,进行以下定义,其实也是代码里要用到的数组:

  • f a u fa_u fau 表示点 u u u 的父亲节点。
  • s u s_u su 表示以点 u u u 为根的子树的节点个数。
  • s o n u son_u sonu 表示 u u u 的重儿子(不知道什么是重儿子没关系,下面会讲)。
  • d u d_u du 表示 u u u 节点的深度。
  • t o p u top_u topu 表示所在重链的顶部节点。

一些定义

  • 重儿子:设点 v v v 是点 u u u 的儿子,若点 v v v 是点 u u u所有儿子中 s v s_v sv 最大的一个,则 v v v u u u 的重儿子。由此易得,当一点 u u u 仅有 1 1 1 个儿子 v v v,那 v v v 就是 u u u 的重儿子。
  • 轻儿子:一节点的子结点中,除了重儿子,其余都为轻儿子。
  • 重链:一条由轻儿子或根节点(即不是重儿子)开头全部由重儿子组成的链(所以一个点也算一条重链)。

举个例子,如下图:黄色的是重儿子,一个紫色圈起来的是一条重链。

我图画得好丑

如何找最近公共祖先?

若我们要找点 a a a b b b 的最近公共祖先

若点 a a a b b b 在同一条重链上

很显然,此时深度小的点是点 a a a b b b 的最近公共祖先。

若点 a a a b b b 不在同一条重链上

这时联想暴力做法,我们会惊喜地发现,只要我们预处理出 t o p top top 数组,就可以利用它直接往上跳,从而避免暴力中的一步一步龟速上爬,大大的提升效率。

有人就问了:但这样不能精确控制上调步数啊?实际上我们不需要精确控制步数,重链的定义就给予了我们极大的便利。

我们只需要不断将深度小的点跳到到重链顶端的父亲上,直至两点在同一重链上,结合图仔细观察并举个例子手模,可得知其正确性。

具体的,我们令 d a ≥ d b d_a \ge d_b dadb,让 a a a 跳到 f a t o p a fa_{top_a} fatopa,重复此操作,直到 t o p a = t o p b top_a = top_b topa=topb。下面是代码实现:

int lca(int a,int b){
   
	while(top[a]!=top[b]){
   
		if(deep[top[a]]<deep[top[b]]) swap(a,b);//强制让d[a]>=d[b]
		a=fa[top[a]];
	}
	return deep[a]<deep[b]?a:b;
}

预处理

上面的代码用到了:

  • f a u fa_u fau 表示点 u u u 的父亲节点。
  • d u d_u du 表示 u u u 节点的深度。
  • t o p u top_u topu 表示所在重链的顶部节点。

要预处理出他们,还需定义两个数组,即上文提到过的 s o n u son_u sonu s u s_u su,定义如下:

  • s u s_u su 表示以点 u u u 为根的子树的节点个数。
  • s o n u son_u sonu 表示 u u u 的重儿子。

我们采用两次 DFS 来预处理。

第一次 DFS

其中, s u s_u su d u d_u du f a u fa_u fau 明显可以对树进行 DFS 来处理。在 DFS 的过程中,到 u u u 点时,我们会遍历 u u u 的所有子节点,此时根据重儿子的定义就能轻松地处理出 s o n u son_u sonu

//Fa是u的父亲
//deep[u]是u点深度,即上文的d_u
//Size[u]即上文的s_u,因为要避免变量名重复
void dfs(int u,int Fa){
   
	fa[u]=Fa,deep[u]=deep[Fa]+1,Size[u]=1;//Size初始化不要忘
	for(int v:e[u]) if(v!=Fa){
   
		dfs(v,u);
		Size[u]+=Size[v];//这排一定要放在DFS处理完Size[v]后,判定重儿子之前
		if(Size[v]>Size[son[u]])//找重儿子
            son[u]=v;
	}
}
第二次 DFS

第一次 DFS 完了后会发现最重要的 t o p top top 数组还没求,然后想办法求 t o p top top 数组。

根据重链的定义可以知:若 u u u 为轻儿子或根节点,那么 t o p u = u top_u=u topu=u,且以它开头的重链上的点 v v v t o p v top_v topv 都为 u u u

由此可以想到再进行一次 DFS,DFS 函数中传递的参数设为当前走到的节点 u u u u u u 节点所在的重链开头的点 r o o t root root。DFS 操作开始将 u u u 赋值给 t o p u top_u topu。单独 DFS 重儿子,下传的 r o o t root root 不变。在遍历所有轻儿子 v v v,下传 r o o t root root 参数为 v v v。代码实现如下:

void Dfs(int u,int root){
   
	top[u]=root;
	if(!son[u]) return ;
	Dfs(son[u],root);
	for(int v:e[u]) if(v!=fa[u]&&v!=son[u]) Dfs(v,v);
}

完整代码

#include<bits/stdc++.h>
using namespace std;
const int N=5e6+5;
int n,Q,s,fa[N],Size[N],deep[N],son[N],top[N];
vector<int> e[N];

void dfs(int u,int Fa){
   
	fa[u]=Fa,deep[u]=deep[Fa]+1,Size[u]=1;
	for(int v:e[u]) if(v!=Fa){
   
		dfs(v,u);
		Size[u]+=Size[v];
		if(Size[v]>Size[son[u]]) son[u]=v;
	}
}

void Dfs(int u,int root){
   
	top[u]=root;
	if(!son[u]) return ;
	Dfs(son[u],root);
	for(int v:e[u]) if(v!=fa[u]&&v!=son[u]) Dfs(v,v);
}

int lca(int a,int b){
   
	while(top[a]!=top[b]){
   
		if(deep[top[a]]<deep[top[b]]) swap(a,b);
		a=fa[top[a]];
	}
	return deep[a]<deep[b]?a:b;
}

int main(){
   
	scanf("%d%d%d",&n,&Q,&s);
	for(int i=1,u,v;i<n;i++){
   
		scanf("%d%d",&u,&v);
		e[u].push_back(v),e[v].push_back(u);
	}
	dfs(s,0);
	Dfs(s,s);
	while(Q--){
   
		int a,b;
		scanf("%d%d",&a,&b);
		printf("%d\n",lca(a,b));
	}
	return 0;
}