1090 字
5 分钟
最近公共祖先

向上标记法#

// 第一次dfs标记根节点的该结点之间所有点
bool dfs(int cur, int fa, int val) {
if (cur == val) {
id[cur] = true;
return true;
}
for (int i = h[cur]; ~i; i = ne[i]) {
int v = e[i];
if (v == fa) continue;
if (dfs(v, cur, val)) {
id[cur] = true;
return true;
}
}
return false;
}
// 第二次dfs遇到有标记的点就是lca(a,b)
int redfs(int cur, int fa, int val) {
if (cur == val) {
if (id[cur]) return cur;
return -1;
}
for (int i = h[cur]; ~i; i = ne[i]) {
int v = e[i];
if (v == fa) continue;
int t = redfs(v, cur, val);
if (t > 0) return t;
else if (t == -1) {
if (id[cur]) return cur;
return -1;
}
}
return 0;
}

倍增法#

预处理 O(nlogn)O(nlogn)

  1. f[i,k]f[i,k]:结点 ii 的第 2k2^k 个祖先 0≤k≤logn0 \le k \le logn,其中 nn 是结点的数量,注意下标从 11 开始,因为这样方便结点 00 充当一个虚拟最大祖先,也就是哨兵的意思
  2. depth[i]depth[i] 表示结点 ii 的深度,注意 depth[i]>0depth[i]>0,因为这样方便 ∀i,depth[i]>depth[0]\forall i, depth[i] > depth[0]

根据预处理求结点 xx 的第 kk 个祖先 对 kk 进行二进制拆分:k=2a1+2a2+⋯+2ajk = 2^{a_1}+2^{a_2}+\cdots+2^{a_j} ,因此求 xx 的第 kk 个祖先就是 f(f(f(x,a1),a2)⋯ ,aj)f(f(f(x,a_1),a_2) \cdots , a_j)

求 lca(x,y)lca(x,y) O(logn)O(logn)

  1. 对齐深度,把深度较高的点,假设为 xx ,找到 xx 的第 depth[x]−depth[y]depth[x]-depth[y] 个祖先结点 x′x^\prime ,这样就对齐了两点的深度。
  2. 同样地,利用二进制拆分将 x′,yx^\prime,y 移动到 lca(x,y)lca(x,y) 的下一个结点,最终再将变化后的 x′x^\prime 或 yy 再往上跳一格,也就是 f[x′][0]f[x^\prime][0] 就是最近公共祖先

代码如下:

/*
dfs调用: dfs(r, 0, 1)
*/
int h[N], e[N << 1], ne[N << 1], idx = 0;
int f[N][18], dep[N];
int n, r, tot = 0;
void dfs(int u, int fa, int cur) {
dep[u] = cur;
f[u][0] = fa;
for (int i = 1; i < 18; i ++ )
f[u][i] = f[f[u][i - 1]][i - 1];
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
if (v == fa) continue;
dfs(v, u, cur + 1);
}
}
// bfs 预处理更加保险,不会爆栈
void bfs() {
queue<int> Q;
Q.push(r);
dep[r] = 1;
st[r] = true;
while (Q.size()) {
int u = Q.front(); Q.pop();
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
if (st[v]) continue;
dep[v] = dep[u] + 1;
f[v][0] = u;
for (int i = 1; i < 18; i ++ )
f[v][i] = f[f[v][i - 1]][i - 1];
Q.push(v);
st[v] = true;
}
}
}
int lca(int x, int y) {
if (dep[x] > dep[y]) swap(x, y);
for (int i = 17; i >= 0; i -- )
if (dep[f[y][i]] >= dep[x])
y = f[y][i];
if (x == y) return x;
for (int i = 17; i >= 0; i -- )
if (f[x][i] != f[y][i])
x = f[x][i], y = f[y][i];
return f[x][0];
}

tarjan 离线算法#

离线算法和在线算法#

离线算法:一般的面向有多次询问的题面,然后一次性把所有询问读入,然后再输出结果的算法 在线算法:即问即答

tarjan 求 lca#

如图所示:在 dfs 的过程中访问到某点时我们可以将点分为三类,其中蓝色的点表示已经访问完的点,绿色的点相当于在递归栈中的点,也就是正在访问的点,白色的点表示未尚未标记的点。

根据这样对点进行划分,有一个结论:如果要求两个点的最近公共祖先,其中一个点在已经访问完的点中,一个点就是当前正在访问的点,那么这些点对的最近公共祖先一定是绿色集合中的点。为此,可以提前将所有询问存储,进而设计一个一次遍历整棵树的同时回答所有询问的离线算法。

算法流程:

  1. 首先存储所有询问,存储方式类似于 vector 写法的邻接表,query[i] 存储所有与点 ii 相关的查询与其查询索引
  2. 然后从任意一个点做 tarjan 算法 2.1 首先遍历当前点的所有孩子,然后遍历完利用并查集把当前孩子的双亲设置为当前结点 2.2 利用并查集求解与这个点所有相关的,另外一个点是已经访问完的所有询问d 2.3 将当前结点标记为已经访问完的状态 代码如下:
int h[N], e[M], ne[M], idx = 0;
int st[N], fa[N];
vector<PII> query[M];
int lca[M];
int n, m;
void tarjan(int u) {
st[u] = 1;
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
if (st[v]) continue;
tarjan(v);
fa[v] = u;
}
for (auto [v, id]: query[u])
if (st[v] == 2)
lca[id] = get(v);
st[u] = 2;
}

RMQ 求 lca#