忠告:别偷懒用 AI(尤其是豆包)写题解,否则你会被格式整崩溃,比如我

题目传送门

欢迎踩博客,数剖做法请见题解:P3379 【模板】最近公共祖先(LCA)树剖

倍增求最近公共祖先

一棵有根 T T T 的两个结点 a a a b b b 的最近公共祖先表示一个结点 x x x,满足 x x x a a a b b b 的祖先且 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),对于多次询问的题目(比如本题)不能解决。

思想

倍增法其实是在上一种方法的基础上进行了优化。我们希望向上查找更快,可以事先预处理出数组 p u , i p_{u,i} pu,i,表示 u u u 往上移动 2 i 2^i 2i 步到达的结点,利用 p p p 数组可以快速的将结点 u u u 向上移动 n n n 步,方法是将 n n n 表示为二进制数。

例如 n = ( 110 ) 2 n = (110)_2 n=(110)2 ,那么利用 p p p 数组先向上移动 2 2 2^2 22 步,然后再继续移动 2 1 2^1 21 步,即 p p u , 2 , 1 p_{p_{u,2},1} ppu,2,1。这样在查找时就不用一个点一个点地往上跳,直接大跳,将大大节约时间。

p p p 数组

有定义可知:

  • j = 0 j=0 j=0 时, p u , i = f a u p_{u,i} = fa_u pu,i=fau,即 p[u][0]=fa_u
  • j ≥ 1 j \ge 1 j1 时, p u , i = p p u , i − 1 , i − 1 p_{u,i} = p_{p_{u,i-1},i-1} pu,i=ppu,i1,i1,即 p[u][i]=p[p[u][i-1]][i-1]

其中, f a u fa_u fau u u u 节点的父亲节点

显然,我们可以利用 DFS 或 BFS 处理出 p p p 数组。下面的代码使用 DFS。

void dfs(int u,int fa){
   //u为当前节点,fa为u节点的父亲
	d[u]=d[fa]+1,p[u][0]=fa;//d[i]表示i节点深度
	for(int i=1;(1<<i)<=d[u];i++)//不能跳出根节点,从小往大处理
		p[u][i]=p[p[u][i-1]][i-1];//核心
	for(int v:e[u]) if(v!=fa) dfs(v,u);
}

求最近公共祖先

现在要求结点 a a a b b b 的最近公共祖先。

a a a b b b 对齐

我们事先准备数组 d u d_u du 表示 u u u 节点的深度。

假设 d a ≤ d b d_a \le d_b dadb。那我们先 b b b 挪到和 a a a 同一深度的位置。代码实现如下:

for(int j=20;i>=0;i--)//要把i从20到0枚举
    if(d[a]<=d[b]-(1<<i))//b向上跳后深度仍不大于a
        b=p[b][i];//b跳到p[b][i]

特别的,若此时 a a a b b b重合,那这时 a a a 点(或 b b b 点)就是 a a a b b b 的最近公共祖先。

if(a==b){
   //若a、b重合
    printf("%d\n",a);
    continue;
}
a a a 点和 b b b 点上跳直到它们重合

a a a b b b 利用 p p p 数组同步上跳,直到它们重合。

for(int j=20;j>=0;j--)//也要把i从20到0枚举
    if(p[a][j]!=p[b][j])//不能重合就继续往上跳
        a=p[a][j],b=p[b][j];
printf("%d\n",p[a][0]);
为什么要把 i i i 20 20 20 0 0 0 枚举?

很好理解,举个例就明白了。如:要从点 u u u 向上跳 666 666 666 步跳到 v v v

过程:把 i i i 20 20 20 0 0 0 枚举(或把 i i i 0 0 0 20 20 20 枚举),若 2 i ≤ x 2^i \le x 2ix (即 d p u , i ≥ d v d_{p_{u,i}} \ge d_{v} dpu,idv),就把 u u u 向上跳 2 i 2^i 2i 步。

  • 若是把 i i i 20 20 20 0 0 0 枚举,上面的例子就会依次跳 2 9 2^9 29 2 7 2^7 27 2 4 2^4 24 2 3 2^3 23 2 1 2^1 21 步,并且 2 9 + 2 7 + 2 4 + 2 3 + 2 1 = 666 2^9+2^7+2^4+2^3+2^1=666 29+27+24+23+21=666,可得把 i i i 20 20 20 0 0 0 枚举是正确的。
  • 若是把 i i i 0 0 0 20 20 20 枚举,上面的例子就会依次跳 2 0 2^0 20 2 2 2^2 22 2 3 2^3 23 2 4 2^4 24 2 5 2^5 25 2 6 2^6 26 2 7 2^7 27 2 8 2^8 28 步,并且 2 0 + 2 1 + 2 3 + 2 4 + 2 5 + 2 6 + 2 7 + 2 8 ≠ 666 2^0+2^1+2^3+2^4+2^5+2^6+2^7+2^8 \ne 666 20+21+23+24+25+26+27+28=666,可得把 i i i 0 0 0 20 20 20 枚举是错误的。

看完这个例子,要把 i i i 20 20 20 0 0 0 枚举的原因就不言而喻了。

完整代码

#include<bits/stdc++.h>
using namespace std;
const int N=5e6+5;
int n,m,s,d[N],p[N][21];
vector<int> e[N];

void dfs(int u,int fa){
   
	d[u]=d[fa]+1,p[u][0]=fa;
	for(int i=1;(1<<i)<=d[u];i++) p[u][i]=p[p[u][i-1]][i-1];
	for(int v:e[u]) if(v!=fa) dfs(v,u);
}

int main(){
   
	scanf("%d%d%d",&n,&m,&s);
	for(int i=1,x,y;i<n;i++){
   
		scanf("%d%d",&x,&y);
		e[x].push_back(y),e[y].push_back(x);
	}
	dfs(s,0);
	for(int i=1,a,b;i<=m;i++){
   
		scanf("%d%d",&a,&b);
		if(d[a]>d[b]) swap(a,b);
		for(int j=20;j>=0;j--) if(d[a]<=d[b]-(1<<j)) b=p[b][j];
		if(a==b){
   printf("%d\n",a);continue;}
		for(int j=20;j>=0;j--) if(p[a][j]!=p[b][j])	a=p[a][j],b=p[b][j];
		printf("%d\n",p[a][0]);
	}
	return 0;
}