NeFut Logo NeFut
Admin Login

Efficient Algorithms for Finding Lowest Common Ancestors in Trees

Published at: 2026-05-27 09:17 Last updated: 2026-06-10 09:29
#algorithm #C++ #LCA

Core Logic and Mathematical Principles

The Lowest Common Ancestor (LCA) is defined as follows: in a rooted tree $T$, for any two nodes $u$ and $v$, the LCA is the deepest node that is an ancestor of both $u$ and $v$, denoted as $\text{LCA}(u, v)$.

The three core paradigms for solving LCA in NOIP each have their own mathematical essence in terms of time and space trade-offs:

1. Binary Lifting (Exponentiation Method)

Based on the idea of binary state compression. By preprocessing each node's $2^k$-th ancestor, we can optimize linear jumps from $O(N)$ to logarithmic jumps $O(\text{log} N)$.

2. Tarjan's Algorithm (Offline Disjoint Set Union)

An offline algorithm that combines depth-first search (DFS) and disjoint set union (DSU).

3. RMQ Transformation Method (Euler Tour + ST Table)

A lossless mapping of the tree structure to a linear algebra maximum value problem.


State Design and Algorithm Derivation

1. Binary Lifting

Define $fa[u][k]$ as the $2^k$-th ancestor of node $u$; $\text{dep}[u]$ is the depth of node $u$.

$$fa[u][k] = fa[fa[u][k-1]][k-1]$$

Physical Meaning: Jumping up $2^k$ steps is equivalent to first jumping up $2^{k-1}$ steps, and then from that position jumping up another $2^{k-1}$ steps.

  1. Assume $\text{dep}[u] \text{≥} \text{dep}[v]$. Use binary lifting to elevate $u$ to the same depth as $v$.
  2. If $u == v$, then $v$ is the LCA.
  3. Otherwise, both $u$ and $v$ attempt to jump upwards by $2^k$ steps. If $fa[u][k] \neq fa[v][k]$, then update $u = fa[u][k], v = fa[v][k]$.
  4. After the loop, $\text{LCA}(u, v) = fa[u][0]$.

2. RMQ Transformation Method

Define $\text{euler}[i]$ as the Euler sequence, and $\text{dep\_seq}[i] = \text{dep}[\text{euler}[i]]$ as the corresponding depth sequence at that position. Use the ST table to maintain the range minimum value: define $f[i][k]$ as the index of the Euler sequence corresponding to the minimum depth value in the depth sequence from position $i$ with length $2^k$.

$$pos\text{_}l = f[i][k-1], \quad pos\text{_}r = f[i + 2^{k-1}][k-1]$$

$$f[i][k] = \text{dep\_seq}[pos\text{_}l] < \text{dep\_seq}[pos\text{_}r] ? pos\text{_}l : pos\text{_}r$$


C++ Standard Source Code (Industrial Template for Binary Lifting)

#include <iostream>
#include <vector>
#include <algorithm>

const int MAXN = 500005;
const int MAXLOG = 21; // 2^20 > 500000, ensuring depth coverage

std::vector<int> adj[MAXN];
int dep[MAXN];
int fa[MAXN][MAXLOG];

// Preprocess depth and $2^0$-th ancestor
void dfs_lca(int u, int p) {
    dep[u] = dep[p] + 1;
    fa[u][0] = p;

    // Dynamic programming state transition, preprocess the binary lifting array
    for (int k = 1; k < MAXLOG; ++k) {
        if (fa[u][k - 1] != 0) {
            fa[u][k] = fa[fa[u][k - 1]][k - 1];
        } else {
            fa[u][k] = 0; // Fatal low-level error prevention: must set to 0 when jumping out of the root node
        }
    }

    for (size_t i = 0; i < adj[u].size(); ++i) {
        int v = adj[u][i];
        if (v == p) continue;
        dfs_lca(v, u);
    }
}

// Online query for LCA
int query_lca(int u, int v) {
    if (dep[u] < dep[v]) std::swap(u, v); // Always ensure u is deeper than v

    // First phase: elevate u to the same depth as v
    for (int k = MAXLOG - 1; k >= 0; --k) {
        if (dep[fa[u][k]] >= dep[v]) {
            u = fa[u][k];
        }
    }

    if (u == v) return u;

    // Second phase: synchronize upwards
    for (int k = MAXLOG - 1; k >= 0; --k) {
        if (fa[u][k] != fa[v][k]) { // Key: only jump when the ancestors differ
            u = fa[u][k];
            v = fa[v][k];
        }
    }

    // Finally stop at the layer below LCA, return the parent node
    return fa[u][0];
}

int main() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);

    int n, m, root;
    if (!(std::cin >> n >> m >> root)) return 0;

    for (int i = 1; i < n; ++i) {
        int u, v;
        std::cin >> u >> v;
        adj[u].push_back(v);
        adj[v].push_back(u);
    }

    // Initialize physical boundary of root node depth, passing 0 as the parent of the root
    dep[0] = 0;
    dfs_lca(root, 0);

    for (int i = 0; i < m; ++i) {
        int u, v;
        std::cin >> u >> v;
        std::cout << query_lca(u, v) << "\n";
    }

    return 0;
}

NOIP Practical Pitfall Guide

  1. Reverse Loop Order in Binary Lifting Depth Elevation: When aligning depths or synchronizing with two pointers upwards, the binary loop must go from large to small (i.e., for (int k = MAXLOG - 1; k >= 0; --k)). If written from small to large, the greedy strategy fails, and the target depth difference cannot be correctly assembled, leading to degraded query results or pointer deadlocks.
  2. No Boundary Protection for Root Node Parent: If the boundary of fa[u][k - 1] == 0 is not correctly handled in dfs_lca, it may lead to accessing fa[0][k - 1]. If dep[0] or the fa[0] array is not cleared or contains dirty data, the entire binary lifting chain may collapse topologically, causing infinite loops or incorrect ancestor positioning.

Classic NOIP/Luogu Problems

1. Luogu P1967 [NOIP2013 Advanced Group] Truck Transportation

2. Luogu P3128 [USACO15JAN] Max Flow P

$$\text{diff}[u] \leftarrow \text{diff}[u] + 1, \quad \text{diff}[v] \leftarrow \text{diff}[v] + 1$$

$$\text{diff}[a] \leftarrow \text{diff}[a] - 1, \quad \text{diff}[fa[a][0]] \leftarrow \text{diff}[fa[a][0]] - 1$$

After modification, run a bottom-up DFS traversal; the true pressure value of each node will be the sum of the differential values of its subtree. The overall time complexity is $O((N + K) \text{log} N)$.

Original Source: local://9.3

[h] Back to Home