最近公共祖先 LCA
最近公共祖先简称 LCA(Lowest Common Ancestor)。两个节点的最近公共祖先,就是这两个点的公共祖先里面,离根最远的那个。LCA是树相关的题中间经常用到的概念和中间步骤,所以透彻理解和灵活运用是比较重要的。
朴素算法:第一步通过DFS算出每个节点的深度;2. 平均两点的深度;3. 两个点都不断往上找父节点,直到相遇。朴素算法复杂度最坏是,但对于随机树,复杂度是。
倍增法(Binary Lifting) 求 LCA
朴素算法最大问题是第2、3步一次移动一层,线性查找复杂度过高,通过预处理,可以加快移动速度。
倍增的具体办法:
- 预先计算祖先指针表:每个节点的上一辈祖先,上两辈祖先,上四辈祖先,上八辈祖先,…,上辈祖先。
 - 平均两点的深度,这里已经可以利用祖先指针表快速移动。
 - 二分查找LCA,从最远的指针开始尝试(可以理解成从高位开始依次尝试改变二进制位),如果不导致路径重合,就follow这个指针(等价于改变对应二进制位)。
 
示例实现:
#include <iostream>
using namespace std;
 
int const lgmax = 20;
int const nmax = 200000;
int fa[1 + nmax][1 + lgmax];  // 每个节点的祖先的倍增指针数组
int level[1 + nmax];          // 节点深度
 
int lca(int x, int y) {
  if(level[x] < level[y])     // 保证x深度更大
    swap(x, y);
  for(int h = lgmax; 0 <= h; h--)
    if(level[y] + (1 << h) <= level[x])
      x = fa[x][h];           // 减少x深度到和y一样
  if(x == y)                  // 如果重合就是LCA
    return x;
  for(int h = lgmax; 0 <= h; h--)
    if(fa[x][h] != fa[y][h]) {  // 从远的指针开始,如果不同,则follow
      x = fa[x][h];
      y = fa[y][h];
    }
  return fa[x][0];
}
 
int main() {
  int n, q;
  cin >> n >> q;
  for(int i = 2; i <= n; i++)
    cin >> fa[i][0];  // 输入每个节点的父节点, 按边输入则用dfs,也类似
 
  for(int h = 1; h <= lgmax; h++)
    for(int i = 1;i <= n; i++)
      fa[i][h] = fa[fa[i][h - 1]][h - 1];   // 准备倍增指针数组
 
  for(int i = 2; i <= n; i++)
    level[i] = level[fa[i][0]] + 1;   // 计算深度
 
  for(int i = 1;i <= q; i++) {
    int x, y;
    cin >> x >> y;
    cout << lca(x, y) << '\n';
  }
}例题
这个问题就是在一棵树里面求任两点的距离,解法就是先求两点的LCA,然后利用以下性质可以直接算出距离:,其中是树上两点间的距离,代表某点到树根的距离。
实现:
#include <cstdio>
#include <cstring>
#include <iostream>
#include <vector>
#define MXN 50007
using namespace std;
std::vector<int> v[MXN];
std::vector<int> w[MXN];
 
// fa是倍增指针,cost是对应指针的路径长度,dep是每个结点的深度
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;
  // 不然的话,找到第一个不是它们共同祖先的两个点,也就是LCA下面一层的两个结点。
  // 这个就是从高位开始依次尝试改变二进制位,如果不导致路径重合,就改变之。
  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;
}
 
int main() {
  // 初始化表示祖先的数组 fa,代价 cost 和深度 dep。
  memset(fa, 0, sizeof(fa));
  memset(cost, 0, sizeof(cost));
  memset(dep, 0, sizeof(dep));
  // 读入树:节点数一共有 n 个。
  scanf("%d", &n);
  for (int i = 1; i < n; ++i) {
    scanf("%d %d %d", &a, &b, &c);
    ++a, ++b;
    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);
  // 查询 m 次,每一次查找两个节点的 lca 点。
  scanf("%d", &m);
  for (int i = 0; i < m; ++i) {
    scanf("%d %d", &a, &b);
    ++a, ++b;
    printf("%d\n", lca(a, b));
  }
  return 0;
}直接调用dfs()完成初始化后,当我们需要查询某点对 的 LCA 时,查询区间 上最小值(pos或depth都可以)的所代表的节点即可。
进一步优化使用Segment Tree线段树,调用dfs()后再调用st_preprocess()即得到查询用的ST树,这时预处理复杂度,查询复杂度。
| 习题 | ||||||
|---|---|---|---|---|---|---|
| P3379 | 【模板】最近公共祖先(LCA) | 普及/提高- | ||||
| P2420 | 让我们异或吧 | 普及/提高- | ||||
| P5836 | USACO Silver | Milk Visits | 2019 | 普及/提高- | ||
| P3128 | USACO Plat | Max Flow | 2015 | 普及+/提高 | ||
| P6869 | COCI | Putovanje | 2019 | 普及+/提高 | ||
| P7103 | 「C.E.L.U-01」族谱树 求第k层孩子的LCA | 普及+/提高 | ||||
| P1967 | NOIP-S | 货车运输 | 提高+/省选- | |||
| P1084 | NOIP-S | 疫情控制 | 2012 | 省选/NOI- | ||
| P1600 | NOIP-S | 天天爱跑步 | 2016 | 省选/NOI- | ||
| P2680 | NOIP-S | 运输计划 | 2015 | 省选/NOI- | ||
| P5838 | USACO Gold | Milk Visits | 2019 | 省选/NOI- | 解答 | |