树上最近公共祖先(LCA)的算法
有错请大力指出【鞠躬】第一次写正经博客非常慌张
LCA(Least Common Ancestors),即最近公共祖先,是指在有根树中,找出某两个结点u和v最近的公共祖先。
对于有根树T的两个结点u、v,最近公共祖先LCA(T,u,v)表示一个结点x,满足x是u、v的祖先且x的深度尽可能大。
另一种理解方式是把T理解为一个无向无环图,而LCA(T,u,v)即u到v的最短路上深度最小的点。
——百度百科
LCA的四种算法:
- 记录dfs序转化为rmq(区间最值)问题(st表)
- tarjan算法
- 倍增算法
- 树链剖分
模板题为洛谷P3379
一、记录dfs序转化为rmq问题
【在线算法】【预处理较慢】【内存较大】【所以到底有什么用】一定是我写得丑

首先我们需要知道dfs序是什么:
其实本人对dfs序的定义也不怎么清晰……望告知orz
首先我们需要一颗树……比如说它长这样:

定义rt(root)为树根,则在这棵树中 rt = 1。别说了我知道图上是A
本题需要用到的dfs序需在进入每个节点即搜完某一个儿子的时候记录这个节点。从rt开始进行dfs(深度优先搜索),则得到的dfs序列为 A B D B E B F B A C G H G C A。
且让我们跟着跑一遍。
1.dfs(A) , 序列为 A
2.找到A的儿子B,dfs(B),序列为 A B
3.找到B的儿子D,dfs(D),序列为 A B D
4.发现D没有儿子,返回B,序列为 A B D B
5.6.7.8. dfs(E),返回B,dfs(F),返回B,序列为 A B D B E B F B
9.搜完B的儿子,返回A,序列为 A B D B E B F B A
10.11.12. dfs(C),dfs(G),dfs(H),序列为 A B D B E B F B A C G H
13.14.15 返回G,返回C,返回A,序列为 A B D B E B F B A C G H G C A
结束。
定义x为当前节点标号,dep为当前深度,h[i]为树高,in[i]为每个节点被访问到的序号,d[i]为dfs序,e[i]为边序号。
此处使用链接表存边,fst[x]表示 x 第一条边的序号,结构体edge{int x , nx ;}中 x 为这条边的另一端点,nx为下一条边的序号。
代码如下:
void dfs(int x , int dep) { d[++ cnt] = x ; h[x] = dep , in[x] = cnt ; for(int i = fst[x] ; i ; i = e[i].nx) { if(e[i].x == fa[x]) continue ; fa[e[i].x] = x ; dfs(e[i].x , dep + 1) ; d[++ cnt] = x ; } }
除了搜完根节点后结束 dfs 外,每个点被搜到和退出时均会在 dfs 序中加入一个数,因此该 dfs 序的长度为2 * n - 1。
设点u、点v的最近公共祖先为点x,并且u在v之前被dfs到。因为x为u、v的祖先,所以dfs时先找到x,再从x往下找到u,即dfs(u)时已经过了x。记录u后,返回到x再向下找到v。故在dfs序中,in[u] 和 in[v] 之间必出现过 x。而dfs(v)后返回时,x 的子树还没有被搜完,所以不会出现 x 的祖先。这样即可保证在 in[u]、in[v] 间高度最小的点即为 u、v 的最近公共祖先。
这样我们就把LCA问题转化为了rmq问题,但如果对于每个询问区间,都一位一位寻找最值,效率为O(mn),显然不能接受。
此处我们就需要用到st表。
定义 mn[i][j] 为从第 i 位起(包括第 i 位),2 ^ j 位中的最小值。询问区间[L , R]最小值时,就从L开始向右跳,每次跳lowbit(R - L + 1)并L += lowbit(R - L + 1),跳log级别即可得出答案。这样就可以O(nlogn)预处理出mn数组并O(1)查询某段区间内的最小值(最大值同理)。
lowbit(x) 即将 x 转为二进制后最低位“1”对应的值。如 lowbit(12) 即将 12 转为 1010 ,lowbit(12) = (10)10 = 2。lowbit的算法为lowbit(x) = x & -x,望自行参透(笑)。注:-x 的二进制表示即 x 的补码,就是将 x 的每一位都反过来再加 1 。
其实我一直不明白为什么st表的查询是O(1),我觉得是O(log)啊……大概是因为我写得丑吧
这样问题就解决了,效率为O(nlogn)也许。
代码如下:
#include<bits/stdc++.h> #define N 500003 using namespace std ; int n , m , x , y , rt , cnt , mi[23] , lg[1<<20|1] , h[N] , fa[N] , in[N] , fst[N] , mn[N * 2][23] ; struct edge { int x , nx ; }e[N * 2] ; void dfs(int x , int dep) { mn[++ cnt][0] = x ; h[x] = dep , in[x] = cnt ; for(int i = fst[x] ; i ; i = e[i].nx) { if(e[i].x == fa[x]) continue ; fa[e[i].x] = x ; dfs(e[i].x , dep + 1) ; mn[++ cnt][0] = x ; } } int find(int l , int r) { int ans = 0 ; if(l > r) l ^= r ^= l ^= r ; int t = r - l + 1 ; for(; t ;) { int k = lg[t & -t] , tmp = mn[l][k] ; ans = h[ans] < h[tmp] ? ans : tmp ; l += mi[k] , t -= mi[k] ; } return ans ; } int main() { mi[0] = 1 ; for(int i = 1 ; i <= 20 ; i ++) mi[i] = mi[i - 1] << 1 , lg[mi[i]] = i ; scanf("%d%d%d" , &n , &m , &rt) ; for(int i = 1 ; i < n ; i ++) { int t1 = i << 1 , t2 = t1 - 1 ; scanf("%d%d" , &x , &y) ; e[t1].x = y , e[t1].nx = fst[x] , fst[x] = t1 ; e[t2].x = x , e[t2].nx = fst[y] , fst[y] = t2 ; } dfs(rt , 1) ; int k = log2(cnt) ; for(int i = 1 ; i <= k ; i ++) for(int j = 1 ; j + mi[i] - 1 <= cnt ; j ++) { int t1 = mn[j][i - 1] , t2 = mn[j + mi[i - 1]][i - 1] ; mn[j][i] = h[t1] < h[t2] ? t1 : t2 ; } h[0] = 1e9 ; for(int i = 1 ; i <= m ; i ++) { scanf("%d%d" , &x , &y) ; printf("%d\n" , find(in[x] , in[y])) ; } return 0; }
【一些奇怪的温馨提示】
- 因为读入时尚不能确定父子关系,边要记为无向边,邻接表数组e记得开两倍。
- 因为需要知道LCA的节点序号而不是高度,mn数组记的是节点标号,比较时通过h数组。
- 询问时两点在dfs序中的顺序不确定,可能需要交换 l , r 。
- l , r 之间的距离记得 + 1 ,第51行处for里面记得 - 1 。
- 坚信不停把cnt打成n的不止我一个……
- 记得初始化h[0]为无限大,因为ans初始化为0,如果h[0]为0,答案无法更新。
- log函数很慢,可以预处理出lg数组(虽然真的很耗空间)。

浙公网安备 33010602011771号