最近公共祖先学习
定义
最近公共祖先简称 LCA(Lowest Common Ancestor)。两个节点的最近公共祖先,就是这两个点的公共祖先里面,离根最远的那个。 为了方便,我们记某点集 \(S=\{v_1,v_2,\ldots,v_n\}\) 的最近公共祖先为\(LCA(v_1, v_2, \dots, v_n)\) 或\(LCA(S)\)。
向上标记法 \(O(N)\)
基本思想就是倍增和二进制拼凑。
\(f[i, j] 表示从 i开始,向上走2^j步所能走到的节点, 0 \le j \le logn\)
\[f(i,j) = father(i), j = 0 \\
f(i,j) = f(f(i, j - 1), j - 1), j \ge 0
\]
\(depth[i] 表示深度\)
哨兵: 如果从\(i\)开始跳\(2^j\)步会跳过根节点,那么\(fa[i, j] = 0\), \(depth[0] = 0\), 并设根节点深度为1, 这时在\(LCA\)函数中我们就能视为不满足条件,继续向低位bit枚举
步骤:
- 先将两个点跳到同一层
- 让两个点同时往上跳,一直跳到它们的最近公共祖先的下一层
预处理\(O(nlogn)\)
查询\(O(logn)\)
Code
#include <bits/stdc++.h>
using i64 = long long;
const int N = 4e4 + 10, M = N * 2;
int n, m;
int h[N], e[M], ne[M], idx;
int depth[N], fa[N][16];
int q[N];
void add(int a, int b) {
ne[idx] = h[a], h[a] = idx, e[idx ++] = b;
}
void bfs(int s) {
memset(depth, 0x3f, sizeof depth);
depth[0] = 0, depth[s] = 1;
std::queue<int> q1;
q1.push(s);
while (q1.size()) {
int t = q1.front(); q1.pop();
for (int i = h[t]; ~i; i = ne[i]) {
int v = e[i];
if (depth[v] > depth[t] + 1) {
q1.push(v);
depth[v] = depth[t] + 1;
fa[v][0] = t;
for (int k = 1; k <= 15; k ++) {
fa[v][k] = fa[fa[v][k - 1]][k - 1];
}
}
}
}
}
int lca(int a, int b) {
if (depth[a] < depth[b]) std::swap(a, b);
for (int k = 15; k >= 0; k --) {
if (depth[fa[a][k]] >= depth[b]) {
a = fa[a][k];
}
}
if (a == b) return a;
for (int k = 15; k >= 0; k --) {
if (fa[a][k] != fa[b][k]) {
a = fa[a][k];
b = fa[b][k];
}
}
return fa[a][0];
}
int main() {
std::cin >> n;
int root = 0;
memset(h, -1, sizeof h);
for (int i = 0; i < n; i ++) {
int a, b;
std::cin >> a >> b;
if (b == -1) root = a;
else add(a, b), add(b, a);
}
bfs(root);
std::cin >> m;
while (m --) {
int a, b;
std::cin >> a >> b;
int k = lca(a, b);
if (k == a) {
std::cout << 1;
} else if (k == b) {
std::cout << 2;
} else std::cout << 0;
puts("");
}
}
Tarjan离线求LCA算法 \(O(n + T)\)
上述复杂度\(n, T\)分别记为节点数和询问数。
在深度优先遍历的时候把点分为三类:
- 已经遍历过,且回溯过的点
- 正在搜索的分支
- 还未搜索到的点
Code
#include <bits/stdc++.h>
using i64 = long long;
const int N = 20010, M = N * 2;
int n, m;
int h[N], e[M], w[M], ne[M], idx;
int p[N], dist[N];
int res[N];
int st[N];
std::vector<std::pair<int, int>> query[N]; // first存查询的另一个点, second存查询编号
void add(int a, int b, int c) {
ne[idx] = h[a], h[a] = idx, e[idx] = b, w[idx ++] = c;
}
int find(int x) {
if (x != p[x]) p[x] = find(p[x]);
return p[x];
}
void dfs(int u, int fa) {
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
if (v == fa) continue;
dist[v] = dist[u] + w[i];
dfs(v, u);
}
}
void tarjan(int u) {
st[u] = 1;
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
if (!st[v]) {
tarjan(v);
p[v] = u;
}
}
for (auto item: query[u]) {
int y = item.first, id = item.second;
if (st[y] == 2) {
int anc = find(y);
res[id] = dist[u] + dist[y] - dist[anc] * 2;
}
}
st[u] = 2;
}
int main() {
std::cin >> n >> m;
memset(h, -1, sizeof h);
for (int i = 0; i < n - 1; i ++) {
int a, b, c;
std::cin >> a >> b >> c;
add(a, b, c), add(b, a, c);
}
for (int i = 0; i < m; i ++) {
int a, b;
std::cin >> a >> b;
if (a != b) {
query[a].push_back({b, i});
query[b].push_back({a, i});
}
}
for (int i = 1; i <= n; i ++) p[i] = i;
dfs(1, -1);
tarjan(1);
for (int i = 0; i < m; i ++) {
std::cout << res[i] << "\n";
}
}
基于RMQ的做法
待补
关于离线算法和在线算法
在线算法:对于询问题,每次读入一个询问就输出
离线算法:对于询问题,必须先把所有询问统一读入,在统一计算,然后统一输出
树上查询两个点的距离
由于树无环,两个点间距离只存在一个,并且存在以下等式:
\[dist[x, y] = dist[x] + dist[y] - dist[LCA(x, y)]
\]
其中\(dist\)表示该点到根节点间的距离。