DSA DSA In Depth
0%
lesson lcabinary-liftingeuler-tour

Lowest Common Ancestor (LCA): Binary Lifting vs Euler Tour + ±1 RMQ Reduction

7 min read Module 3.3: Range Query & Sublinear Decomposition Structures Updated Oct 23, 2026
📄 Cheat Sheet
Lowest Common Ancestor (LCA) — Euler Tour & RMQ Reduction + Binary Lifting
WilliamFiset Lowest Common Ancestor (LCA) — Euler Tour & RMQ Reduction + Binary Lifting Watch on YouTube ↗

Clearly contrasts O(log N) binary lifting jump pointers against the O(1) query Euler Tour + Sparse Table RMQ reduction.

0:00 / --:--

1. Paradigm A: Binary Lifting ($2^k$ Jump Pointers & Path Aggregates)

Binary Lifting stores a table $\text{up}[k][u]$ for $0 \le k \le \lfloor \log_2 N \rfloor$, where $\text{up}[0][u] = \text{parent}[u]$ and $\text{up}[k][u] = \text{up}[k-1][\text{up}[k-1][u]]$. Combined with entry/exit timestamps ($\text{tin}[u], \text{tout}[u]$), we can test whether $u$ is an ancestor of $v$ in $O(1)$ time:

$$\text{isAncestor}(u, v) \iff \text{tin}[u] \le \text{tin}[v] \;\land\; \text{tout}[v] \le \text{tout}[u]$$

To find $\text{LCA}(u, v)$ when neither is an ancestor of the other, iterate $k$ from $\lfloor \log_2 N \rfloor$ down to $0$: if $\text{up}[k][u]$ is not an ancestor of $v$, jump $u \leftarrow \text{up}[k][u]$. After the loop, $u$ is the immediate child of $\text{LCA}(u, v)$ along the path, so $\text{up}[0][u]$ is the exact LCA!

Euler Tour RMQ Invariant (Harel & Tarjan 1984 / Bender & Farach-Colton 2000): Let $E[0 \dots 2N-2]$ be the sequence of vertices visited in an Euler tour of $T$ (appending $u$ upon entering $u$ and again upon returning to $u$ from each child), and let $\text{first}[u]$ be the index of $u$'s first appearance in $E$. Assuming $\text{first}[u] \le \text{first}[v]$, the Lowest Common Ancestor is the unique vertex minimizing depth in $E[\text{first}[u] \dots \text{first}[v]]$: $$\text{LCA}(u, v) = \arg\min_{w \in E[\text{first}[u] \dots \text{first}[v]]} \text{depth}[w]$$ Moreover, adjacent depths in $E$ differ by exactly $+1$ or $-1$ ($|\text{depth}[E[i]] - \text{depth}[E[i-1]]| = 1$). By partitioning $E$ into blocks of size $b = \frac{1}{2}\log_2 N$ and precomputing all $2^{b-1} = O(\sqrt{N})$ normalized $\pm 1$ block shapes (the Method of Four Russians), LCA achieves optimal $\Theta(N)$ build and $\Theta(1)$ query time!
Euler Tour LCA Reduction: LCA(4, 5) = argmin_depth in E[first[4] .. first[5]] = node 2 1 2 3 4 5 Index i: 0 1 2 3 4 5 6 7 8 Euler E: [1] [2] [4] [2] [5] [2] [1] [3] [1] Depth D: 0 1 2 1 2 1 0 1 0 first[4]=2, first[5]=4 -> min depth in E[2..4] is at i=3 (Node 2, depth 1) Notice adjacent depths: |D[i] - D[i-1]| = 1 everywhere (±1 RMQ)
flowchart LR subgraph BinaryLifting["Binary Lifting (up[k][u])"] BL1["Build: O(N log N) Query LCA: O(log N) Supports kthAncestor(u, k) & Path Max/Min Edge"] end subgraph EulerRMQ["Euler Tour + Sparse Table"] ER1["Build: O(N log N) Query LCA: O(1) 2N - 1 Euler states Ideal when Q >> N"] end
Hardware & Cache Reality — Packing (depth << 20) | node into a Single 32-bit Integer: In the Euler Tour RMQ approach, instead of looking up depth[euler[i]] via a double pointer indirection inside your Sparse Table comparison, pack each Euler tour entry as a single 32-bit integer packed[i] = (depth[u] << 20) | u (for $N < 2^{20} \approx 10^6$). Now Math.min(a, b) automatically compares by depth first and breaks ties, and the winning node ID is extracted via minVal & ((1 << 20) - 1) with zero extra array lookups!
Sparse Table O(1) Idempotent RMQ & Euler-Tour / Binary-Lifting LCA
O(N log N) Build · O(1) RMQ & LCA

See why idempotent operations (min, max, gcd) answer range queries in O(1) time via two overlapping power-of-two blocks st[k][L] and st[k][R − 2^k + 1], and how LCA(u, v) reduces to RMQ on the Euler Tour depth array.

Step 1 / 8
Custom Input:
Timeline

Invariant & State

Step 1 (Compute Scale k): For query [L=1, R=6], length = R − L + 1 = 6. Largest power of 2 ≤ 6 is 2^k = 4 (k = ⌊log₂ 6⌋ = 2).
🌳 Live 2D Hierarchical Tree Topology — Sparse Table O(1) Idempotent RMQ & Euler-Tour / Binary-Lifting LCA Visited/Active Pivot/Root Resolved
A[0]=7 [0] L = 1 A[1]=2 [1] A[2]=3 [2] A[3]=0 [3] A[4]=5 [4] A[5]=… [5] R = 6 (l A[6]=3 [6] A[7]=… [7]
[0]A[0]=7
[1]A[1]=2L = 1
[2]A[2]=3
[3]A[3]=0
[4]A[4]=5
[5]A[5]=10
[6]A[6]=3R = 6 (len = 6, k = ⌊log₂ 6⌋ = 2)
[7]A[7]=12
Query Interval [L, R]
[1, 6] (len = 6)
Scale k = ⌊log₂ len⌋
k = 2 (2^k = 4)
Overlapping Lookup
min(st[2][1], st[2][3]) = 0
Query Time
O(1) (2 Table Reads)
[Step 1] Precomputed lg[6] = 2 in O(1). Two blocks of length 2² = 4 will completely cover [1..6]: prefix [1..4] and suffix [3..6].

2. Production Implementation: Binary Lifting (lca, kthAncestor, dist) & $O(1)$ Euler Tour RMQ

/**
 * 1. Binary Lifting LCA + K-th Ancestor + Tree Distance
 * Build: O(N log N) | Query LCA / kthAncestor / dist: O(log N)
 */
class BinaryLiftingLCA {
  constructor(n, adj, root = 0) {
    this.n = n;
    this.LOG = Math.max(1, 32 - Math.clz32(n));
    this.up = Array.from({ length: this.LOG }, () => new Int32Array(n));
    this.depth = new Int32Array(n);
    this.tin = new Int32Array(n);
    this.tout = new Int32Array(n);
    this._build(adj, root);
  }

  _build(adj, root) {
    let timer = 0;
    // Iterative DFS to avoid call-stack overflow on deep trees (N = 2e5)
    const stack = [[root, root, 0]]; // [u, parent, childIdx]
    while (stack.length > 0) {
      const top = stack[stack.length - 1];
      const u = top[0], p = top[1], idx = top[2];
      if (idx === 0) {
        this.tin[u] = ++timer;
        this.up[0][u] = p;
        for (let k = 1; k < this.LOG; k++) {
          this.up[k][u] = this.up[k - 1][this.up[k - 1][u]];
        }
      }
      const neighbors = adj[u];
      if (idx < neighbors.length) {
        const v = neighbors[idx];
        top[2]++;
        if (v !== p) {
          this.depth[v] = this.depth[u] + 1;
          stack.push([v, u, 0]);
        }
      } else {
        this.tout[u] = ++timer;
        stack.pop();
      }
    }
  }

  isAncestor(u, v) {
    return this.tin[u] <= this.tin[v] && this.tout[v] <= this.tout[u];
  }

  kthAncestor(u, k) {
    if (k > this.depth[u]) return -1;
    for (let bit = 0; k > 0; bit++, k >>= 1) {
      if (k & 1) u = this.up[bit][u];
    }
    return u;
  }

  lca(u, v) {
    if (this.isAncestor(u, v)) return u;
    if (this.isAncestor(v, u)) return v;
    for (let k = this.LOG - 1; k >= 0; k--) {
      if (!this.isAncestor(this.up[k][u], v)) {
        u = this.up[k][u];
      }
    }
    return this.up[0][u];
  }

  dist(u, v) {
    const w = this.lca(u, v);
    return this.depth[u] + this.depth[v] - 2 * this.depth[w];
  }
}

3. Complexity Matrix

LCA Algorithm Preprocessing Time & Space lca(u, v) Query Time Supports kthAncestor(u, k) & Path Min/Max Edge?
Binary Lifting (up[k][u]) $\Theta(N \log N)$ time & space $\Theta(\log N)$ Yes ($\Theta(\log N)$)
Euler Tour + Standard Sparse Table $\Theta(N \log N)$ time & space $\Theta(1)$ No (requires LA structure)
Euler Tour + $\pm 1$ RMQ (Bender & Farach-Colton) $\Theta(N)$ time & space $\Theta(1)$ No
Tarjan's Offline Disjoint-Set LCA $\Theta(N + Q)$ space $\Theta((N + Q)\alpha(N))$ offline No
Heavy-Light Decomposition (HLD Jump) $\Theta(N)$ time & space $O(\log N)$ Yes ($O(\log N)$ via DFS tin array)
Common Pitfall — Subtree Euler Tour ($N$ Entries) vs Full LCA Euler Tour ($2N - 1$ Entries): Do not confuse the $N$-element preorder traversal tin[u] (used for subtree queries) with the $(2N - 1)$-element Euler Tour E[0..2N-2] required for RMQ-LCA! In an $N$-element preorder array, the LCA of $u$ and $v$ does not appear between tin[u] and tin[v] (it appears before both of them!). Only when you re-append parent $u$ to E[] after returning from every child subtree does the LCA appear inside $[\text{first}[u], \text{first}[v]]$.
Check yourself Question 1 of 4
Concept Check Tree Distance Formula via Lowest Common Ancestor recall

In a rooted tree where `depth[u]` is the distance (or sum of edge weights) from the root to vertex $u$, how is the shortest path distance $\text{dist}(u, v)$ between any two vertices $u$ and $v$ computed using $\text{LCA}(u, v)$?

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

Identify the key structural property of the Euler Tour depth array and how the Method of Four Russians exploits it:

Key Takeaways

Binary Lifting stores 2^k jump pointers up[k][u] = up[k-1][up[k-1][u]], answering both LCA(u, v) and kthAncestor(u, k) in O(log N) time.
Using entry/exit timestamps (tin[u] <= tin[v] && tout[v] <= tout[u]) checks ancestry in O(1) time and simplifies binary lifting.
Recording a vertex every time an Euler Tour enters it or returns from a child produces an array E[0..2N-2] where LCA(u, v) is the minimum-depth vertex in E[first[u] .. first[v]].
Because adjacent depths in the Euler Tour differ by exactly +1 or -1, Sparse Table (O(N log N) build) or Four Russians ±1 RMQ (O(N) build) answers LCA queries in O(1) time.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue