学习笔记:图论-LCA

· · 个人记录

概念

对于有根树T的两个结点uv,最近公共祖先 LCA(T,u,v) 表示一个结点x,满足xuv的祖先且x的深度尽可能大。在这里,一个节点也可以是它自己的祖先。

分析一:离线-tarjan

- 起初,每个点的并查集源是自己;在结束对一个点和其子树的遍历后,将其与其父亲合并。 - 搜索完一个点的所有子树后,检查与此点相关的询问,假设当前点为$u$,其询问的点为$v$,若发现$v$已经被搜索过($vis[v]==1$),那么$LCA(u,v) = f[v]$//此处$f[v]$表示$v$的并查集源。 ### 证明&实现: 见[ZJL_OIJR大大关于离线LCA的博客](https://segmentfault.com/a/1190000015145319?utm_source=index-hottest) ## 分析二:在线-倍增 倍增算法需要$O(NlogN)$的预处理,存下每个点的祖先信息,然后以每次$O(logN)$的时间复杂度查询。 - 预处理$step1$:声明数组$anc[N][log_2N]$,其中,$anc[i][j]$表示$i$号节点的第$2^j$个祖先编号(与$ST$表类似)。 - 预处理$step2$:我们用$dfs$遍历全图,同时记录深度。对于$∀u∈G$,$anc[u][0]=father[u]$,然后从当前点向上处理,即 ```cpp anc[u][i] = anc[anc[u][i-1]][i-1] ``` - 查询$step1$:假设询问的点为$x$和$y$。首先,我们令$x$成为深度较大的那个点(便于操作),然后使$x$不停向上跳,直到与$y$深度相同。这时,**倍增**的优势就显现出来了:一层一层跳当然不会错,但是效率低,所以采用一次跳$2^i$格,并且$i$从大到小,每次都尽可能多地跳,这样就可以刚好找到并且复杂度由$O(N)$降为$O(logN)$了。 ```cpp for(int i = 19; i >= 0; i--) { if(dep[x] - (1<<i) <= dep[y]) x = anc[x][i]; } if(x==y) return x;//如果已经重合,就直接返回即可 ``` - **注意:** 此处一定要记得特判,这种情况就相当于$y$就是$x$的祖先;如果不特判,继续如下操作则会出错。 - 查询$step2$:现在 $x$ 和 $y$ 已经在同一层,那么显然我们要把他俩一起向上跳,直到刚好重合(不多也不能少)。一起跳很容易实现,但怎么实现刚好重合呢?如果我们写$if(anc[x][i] == anc[y][i])$就往上跳,那很有可能会跳多,就是说跳到了 LCA 的上方。所以,我们应该把$==$改成$!=$,才能保证一直跳在 LCA 下方。这样运行结束后, LCA 就刚好是$x$和$y$的父亲了。 ```cpp for(int i = 19; i >= 0; i--) { if(anc[x][i]!=anc[y][i]) { x = anc[x][i]; y = anc[y][i]; } } return anc[x][0]; ``` 利用好倍增,查询就是$O(logN)$的时间复杂度了。 ## 代码 ```cpp /********************************** Problem: luogu - P3379 - 【模板】最近公共祖先(LCA) State: Accepted From: 【模板】 Algorithm: LCA Author: cyh_toby Last updated on 2020.6.16 **********************************/ #include <cstdio> #include <algorithm> #include <cmath> using namespace std; const int N = 5e5+5, M = 5e5+5; #define gc getchar int n, m, rt; int g[N], v[2*M], nxt[2*M], tot; int anc[N][20], dep[N]; inline int rd() { int x = 0, f = 1; char c = gc(); while (c < '0' || c > '9') { if (c == '-') f = -1; c = gc(); } while (c >= '0' && c <= '9') { x = (x << 1) + (x << 3) + c - '0'; c = gc(); } return x * f; } inline void add(int x, int y) { v[++tot] = y, nxt[tot] = g[x], g[x] = tot; return; } void dfs(int u, int fa, int depth) { anc[u][0] = fa, dep[u] = depth; for (int i = 1; (1<<i) <= depth; i++) anc[u][i] = anc[anc[u][i-1]][i-1]; for (int i = g[u]; i; i = nxt[i]) if (anc[u][0] != v[i]) dfs(v[i], u, depth+1); return; } inline int lca(int x, int y) { if (dep[x] < dep[y]) swap(x, y); int derta = dep[x] - dep[y]; for (int i = 0; (1 << i) <= derta; i++)//将 derta 二进制分解再进行处理 if (derta & (1 << i) ) x = anc[x][i]; if (x == y) return x; int t = log2((double)dep[x]); for (int i = t; i >= 0; i--) if (anc[x][i] != anc[y][i]) x = anc[x][i], y = anc[y][i]; return anc[x][0]; } int main() { n = rd(), m = rd(), rt = rd(); for (int i = 1; i <= n-1; i++) { int x = rd(), y = rd(); add(x, y); add(y, x); } dfs(rt, 0, 0); for (int i = 1; i <= m; i++) printf("%d\n", lca(rd(), rd())); return 0; } ``` 可以丢个传送门[【模板】LCA](https://www.luogu.com.cn/problem/P3379) ## 小结 个人认为倍增会比 tarjan 更实用。 其实倍增重要的并不是算法,而是**倍增思路和技巧**,**切忌死搬硬套**。掌握了**倍增思想**可以更有助于破题。