LogoCSP Wiki By Yundou
4 Graph

最近公共祖先LCA

定义

最近公共祖先简称 LCA(Lowest Common Ancestor)。两个节点的最近公共祖先,就是这两个点的公共祖先里面,离根最远的那个。 为了方便,我们记某点集 S={v1,v2,,vn}S=\{v_1,v_2,\ldots,v_n\} 的最近公共祖先为 LCA(v1,v2,,vn)\text{LCA}(v_1,v_2,\ldots,v_n)LCA(S)\text{LCA}(S)

性质

  1. LCA({u})=u\text{LCA}(\{u\})=u
  2. uuvv 的祖先,当且仅当 LCA(u,v)=u\text{LCA}(u,v)=u
  3. 如果 uu 不为 vv 的祖先并且 vv 不为 uu 的祖先,那么 u,vu,v 分别处于 LCA(u,v)\text{LCA}(u,v) 的两棵不同子树中;
  4. 前序遍历中,LCA(S)\text{LCA}(S) 出现在所有 SS 中元素之前,后序遍历中 LCA(S)\text{LCA}(S) 则出现在所有 SS 中元素之后;
  5. 两点集并的最近公共祖先为两点集分别的最近公共祖先的最近公共祖先,即 LCA(AB)=LCA(LCA(A),LCA(B))\text{LCA}(A\cup B)=\text{LCA}(\text{LCA}(A), \text{LCA}(B))
  6. 两点的最近公共祖先必定处在树上两点间的最短路上;
  7. d(u,v)=h(u)+h(v)2h(LCA(u,v))d(u,v)=h(u)+h(v)-2h(\text{LCA}(u,v)),其中 dd 是树上两点间的距离,hh 代表某点到树根的距离。

求法

朴素算法

过程

可以每次找深度比较大的那个点,让它向上跳。显然在树上,这两个点最后一定会相遇,相遇的位置就是想要求的 LCA。 或者先向上调整深度较大的点,令他们深度相同,然后再共同向上跳转,最后也一定会相遇。

性质

朴素算法预处理时需要 dfs 整棵树,时间复杂度为 O(n)O(n),单次查询时间复杂度为 Θ(n)\Theta(n)。如果树满足随机性质,则时间复杂度与这种随机树的期望高度有关。

倍增算法

过程

倍增算法是最经典的 LCA 求法,他是朴素算法的改进算法。通过预处理 fax,i\text{fa}_{x,i} 数组,游标可以快速移动,大幅减少了游标跳转次数。fax,i\text{fa}_{x,i} 表示点 xx 的第 2i2^i 个祖先。fax,i\text{fa}_{x,i} 数组可以通过 dfs 预处理出来。

现在我们看看如何优化这些跳转: 在调整游标的第一阶段中,我们要将 u,vu,v 两点跳转到同一深度。我们可以计算出 u,vu,v 两点的深度之差,设其为 yy。通过将 yy 进行二进制拆分,我们将 yy 次游标跳转优化为「yy 的二进制表示所含 1 的个数」次游标跳转。 在第二阶段中,我们从最大的 ii 开始循环尝试,一直尝试到 00(包括 00),如果 fau,ifav,i\text{fa}_{u,i}\not=\text{fa}_{v,i},则 ufau,i,vfav,iu\gets\text{fa}_{u,i},v\gets\text{fa}_{v,i},那么最后的 LCA 为 fau,0\text{fa}_{u,0}

图片描述

复杂度

倍增算法的预处理时间复杂度为 O(nlogn)O(n \log n),单次查询时间复杂度为 O(logn)O(\log n)。 另外倍增算法可以通过交换 fa 数组的两维使较小维放在前面。这样可以减少 cache miss 次数,提高程序效率。

倍增求LCA模板

#include <cstring>
#include <iostream>
#include <vector>
 
constexpr int MXN = 40005;
using namespace std;
vector<int> v[MXN];
vector<int> w[MXN];
 
int fa[MXN][31], cost[MXN][31], dep[MXN];
int n, m;
int a, b, c;
 
// dfs,用来为 lca 算法做准备。接受两个参数:dfs 起始节点和它的父亲节点。
void dfs(int root, int fno) {
  // 初始化:第 2^0 = 1 个祖先就是它的父亲节点,dep 也比父亲节点多 1。
  fa[root][0] = fno;
  dep[root] = dep[fa[root][0]] + 1;
  // 初始化:其他的祖先节点:第 2^i 的祖先节点是第 2^(i-1) 的祖先节点的第
  // 2^(i-1) 的祖先节点。
  for (int i = 1; i < 31; ++i) {
    fa[root][i] = fa[fa[root][i - 1]][i - 1];
    cost[root][i] = cost[fa[root][i - 1]][i - 1] + cost[root][i - 1];
  }
  // 遍历子节点来进行 dfs。
  int sz = v[root].size();
  for (int i = 0; i < sz; ++i) {
    if (v[root][i] == fno) continue;
    cost[v[root][i]][0] = w[root][i];
    dfs(v[root][i], root);
  }
}
 
// lca。用倍增算法算取 x 和 y 的 lca 节点。
int lca(int x, int y) {
  // 令 y 比 x 深。
  if (dep[x] > dep[y]) swap(x, y);
  // 令 y 和 x 在一个深度。
  int tmp = dep[y] - dep[x], ans = 0;
  for (int j = 0; tmp; ++j, tmp >>= 1)
    if (tmp & 1) ans += cost[y][j], y = fa[y][j];
  // 如果这个时候 y = x,那么 x,y 就都是它们自己的祖先。
  if (y == x) return ans;
  // 不然的话,找到第一个不是它们祖先的两个点。
  for (int j = 30; j >= 0 && y != x; --j) {
    if (fa[x][j] != fa[y][j]) {
      ans += cost[x][j] + cost[y][j];
      x = fa[x][j];
      y = fa[y][j];
    }
  }
  // 返回结果。
  ans += cost[x][0] + cost[y][0];
  return ans;
}
 
void Solve() {
  cin.tie(nullptr)->sync_with_stdio(false);
  // 初始化表示祖先的数组 fa,代价 cost 和深度 dep。
  memset(fa, 0, sizeof(fa));
  memset(cost, 0, sizeof(cost));
  memset(dep, 0, sizeof(dep));
  // 读入树:节点数一共有 n 个,查询 m 次,每一次查找两个节点的 lca 点。
  cin >> n >> m;
  // 初始化树边和边权
  for (int i = 1; i <= n; ++i) {
    v[i].clear();
    w[i].clear();
  }
  for (int i = 1; i < n; ++i) {
    cin >> a >> b >> c;
    v[a].push_back(b);
    v[b].push_back(a);
    w[a].push_back(c);
    w[b].push_back(c);
  }
  // 为了计算 lca 而使用 dfs。
  dfs(1, 0);
  for (int i = 0; i < m; ++i) {
    cin >> a >> b;
    cout << lca(a, b) << '\n';
  }
}
 
int main() {
  cin.tie(nullptr)->sync_with_stdio(false);
  int T;
  cin >> T;
  while (T--) Solve();
  return 0;
}

例题:

On this page