Lowest Common Ancestor (LCA) — Junior Level¶
One-line summary: The Lowest Common Ancestor of two nodes
uandvin a rooted tree is the deepest node that is an ancestor of both. The naive method climbs the tree inO(N)per query; binary lifting precomputes2^k-th ancestors so each query runs inO(log N)after anO(N log N)build.
Table of Contents¶
- Introduction
- Prerequisites
- Glossary
- Core Concepts
- Big-O Summary
- Real-World Analogies
- Pros & Cons
- Step-by-Step Walkthrough
- Code Examples
- Coding Patterns
- Error Handling
- Performance Tips
- Best Practices
- Edge Cases & Pitfalls
- Common Mistakes
- Cheat Sheet
- Visual Animation
- Summary
- Further Reading
Introduction¶
Imagine a family tree, an organization chart, or a folder hierarchy. Two people (or two folders) sit somewhere in the structure, and you want the closest shared parent — the lowest point from which you can reach both by going down. That point is the Lowest Common Ancestor (LCA).
Formally, given a tree rooted at some node root, the LCA of two nodes u and v is the deepest node w such that w is an ancestor of both u and v. ("Ancestor" includes the node itself: a node is its own ancestor.) Because a rooted tree has a unique path between any two nodes, and that path bends at exactly one node, the LCA is always unique and always exists (in the worst case it is the root).
The LCA is one of the most reused building blocks in tree algorithms:
- Distance between two nodes:
dist(u, v) = depth[u] + depth[v] − 2 · depth[lca(u, v)]. - Path queries (the maximum edge on the path, the sum of weights, etc.) when combined with heavy-light decomposition (a sibling topic).
- k-th ancestor of a node — "who is 5 levels above me?"
- Auxiliary / virtual trees, dominator trees, and many competitive-programming patterns.
This file teaches two online methods — methods that answer each query immediately, with no need to know all queries up front:
- The naive equalize-then-climb method: simple,
O(N)per query, great for understanding. - Binary lifting: the workhorse. After an
O(N log N)preprocessing pass it answers each query inO(log N).
There is also a classic offline method (Tarjan's, using a disjoint-set / union-find), which processes all queries together. That one has its own dedicated topic at 12-disjoint-set / 04-offline-lca — we point to it and concentrate here on the online methods.
Prerequisites¶
Before this file you should be comfortable with:
- Rooted trees — parent, child, root, leaf, ancestor, descendant.
- Depth-First Search (DFS) — we compute depths and parents with one DFS.
- Arrays and 2-D arrays — binary lifting stores a table
up[k][v]. - Big-O notation —
O(N),O(log N),O(N log N). - Bit operations —
1 << k, and reading the bits of a number (used to decompose a climb of lengthdinto powers of two). - Recursion or an explicit stack — for the preprocessing DFS.
Optional but helpful:
- Familiarity with the idea of a sparse table (precomputed
2^k-jump tables) from range queries. - Having seen BFS — you can compute depths with BFS instead of DFS.
Glossary¶
| Term | Meaning |
|---|---|
| Rooted tree | A tree with one node designated the root; every other node has a unique parent. |
| Ancestor | a is an ancestor of v if a lies on the path from v up to the root (a node is its own ancestor). |
| Descendant | The inverse of ancestor. |
| Depth / level | Number of edges from the root to the node. The root has depth 0. |
| LCA(u, v) | The deepest node that is an ancestor of both u and v. |
| k-th ancestor | The node reached by going up k parent links from v. |
| Binary lifting | A technique storing up[k][v] = the 2^k-th ancestor of v, enabling O(log N) jumps. |
| Sparse table | A precomputed table of 2^k-sized aggregates; binary lifting is a sparse table over the parent function. |
| Euler tour | A DFS traversal that records nodes as it enters and leaves them, flattening the tree into an array. |
| RMQ | Range Minimum Query — find the minimum in a subarray; LCA reduces to this on the Euler tour. |
| Online query | A query answered immediately, without needing future queries. |
| Offline query | A query that may be reordered and answered in a batch (e.g., Tarjan's LCA). |
| Sentinel | A placeholder value (often -1 or 0) used for "the parent of the root," which does not exist. |
Core Concepts¶
1. What "ancestor" and "depth" mean¶
Root the tree at node 0 (any node works). Run a DFS. For each node v record:
parent[v]— the immediate parent (parent[root] = -1).depth[v]— the number of edges from the root.
LCA(4, 5) = 1 (both hang under 1). LCA(4, 7) = 0 (their paths only meet at the root). LCA(7, 3) = 3 (3 is an ancestor of 7 — and an ancestor of itself).
2. The naive idea — equalize depth, then climb together¶
Two observations make the naive method work:
- If
uis deeper thanv, bringuup to the same depth asvby following parent links. The LCA is unchanged because moving a node toward the root never skips past the LCA — as long as we do not go above it. - Once
uandvare at the same depth, move both up one step at a time. The first time they land on the same node, that node is the LCA.
equalize: while depth[u] > depth[v]: u = parent[u]
while depth[v] > depth[u]: v = parent[v]
climb: while u != v:
u = parent[u]
v = parent[v]
return u
This is O(N) per query in the worst case (a long path tree). Correct and simple — but too slow when you have many queries.
3. Binary lifting — jump in powers of two¶
The naive climb takes single steps. Binary lifting lets you take big jumps. Precompute a table:
up[0][v] = parent[v] // 1 step up = 2^0
up[k][v] = up[k-1][ up[k-1][v] ] // 2^k steps = two jumps of 2^(k-1)
up[k][v] is the 2^k-th ancestor of v. With this table you can move any node up by any distance d in O(log N): write d in binary and, for every set bit k, jump 2^k.
The LCA query then becomes:
- Lift the deeper node up by
depth[u] − depth[v]so both are at equal depth. - If they are now equal, that node is the LCA.
- Otherwise, for
kfrom high to low: ifup[k][u] != up[k][v], jump both up by2^k. After the loop,uandvsit exactly one step below the LCA, so the answer isparent[u](=up[0][u]).
Building the table is O(N log N) time and space; each query is O(log N).
4. Sizing the table — LOG¶
LOG = ceil(log2(N)) (use LOG = ceil(log2(N)) + 1 to be safe). For N up to 10^5, LOG = 17 is plenty (2^17 = 131072). The root's ancestors past the top must point to a sentinel (commonly -1 or the root itself) so jumps that "overshoot" stop cleanly.
Big-O Summary¶
| Method | Build time | Build space | Query time | Online? | Notes |
|---|---|---|---|---|---|
| Naive (equalize + climb) | O(N) (one DFS for depth) | O(N) | O(N) worst | Yes | Simplest; fine for few queries. |
| Binary lifting | O(N log N) | O(N log N) | O(log N) | Yes | The standard online method. Also gives k-th ancestor. |
| Euler tour + sparse table RMQ | O(N log N) | O(N log N) | O(1) | Yes | Fastest queries; reduces LCA to RMQ. |
| Farach-Colton–Bender (±1 RMQ) | O(N) | O(N) | O(1) | Yes | Optimal asymptotics; more involved to implement. |
| Tarjan offline (union-find) | O((N + Q) α(N)) | O(N + Q) | amortized near-O(1) | No | All queries known up front. See 12-disjoint-set / 04-offline-lca. |
N = number of nodes, Q = number of queries, α = inverse Ackermann (effectively constant).
Real-World Analogies¶
| Concept | Analogy |
|---|---|
| LCA in a family tree | Two cousins want to know their most recent common ancestor — the grandparent (or great-grandparent) from whom both descend. The "lowest" such ancestor is the closest one. |
| LCA in an org chart | Two employees in different teams ask: "who is the lowest-level manager that both of us report to?" That manager is the LCA. |
| Equalize-then-climb | Two people stand on different floors of a building's family staircase. First the one higher up walks down to the same floor; then both walk up together until they meet at a landing. |
| Binary lifting | Instead of climbing one stair at a time, you take an elevator that stops at floors +1, +2, +4, +8, … above you, so you reach floor +13 in three hops (8 + 4 + 1). |
| Sentinel above the root | The CEO has no manager — asking "the CEO's manager" returns a "none" placeholder, not a crash. |
Where the analogy breaks: a real family tree can have two parents per person (a DAG), but LCA as defined here needs a rooted tree with a single parent per node. Multi-parent LCA is a harder, separate problem.
Pros & Cons¶
| Pros | Cons |
|---|---|
Binary lifting answers each query in O(log N) — fast enough for 10^5+ queries. | The table costs O(N log N) memory; large N can be heavy. |
| One preprocessing DFS gives depths, parents, and the table in one pass. | The preprocessing DFS can blow the recursion stack on deep/path-like trees. |
| Binary lifting also solves k-th ancestor and weighted path-aggregate queries for free. | Off-by-one bugs in LOG sizing, depth indexing, and the sentinel are common. |
| Online: handle queries as they arrive, interleaved with reads. | If you only need distances and can batch queries, Tarjan offline is simpler asymptotically. |
Euler+RMQ gives O(1) queries — best when you have huge query volumes. | The tree must be static; updates (re-rooting, adding edges) invalidate the table. |
When to use online LCA: many (u, v) queries on a fixed rooted tree; you need distances, k-th ancestor, or path aggregates; queries arrive one at a time.
When NOT to use it: the tree changes between queries (use link-cut trees), or all queries are known up front and you only need plain LCA (Tarjan offline is leaner).
Step-by-Step Walkthrough¶
Use this tree (rooted at 0):
parent = [-1, 0, 0, 1, 1, 2, 4], depth = [0, 1, 1, 2, 2, 2, 3].
Naive: LCA(6, 3)¶
depth[6] = 3, depth[3] = 2. 6 is deeper.
equalize: 6 -> parent[6] = 4 (now both at depth 2: u=4, v=3)
climb: 4 != 3 -> 4 -> parent=1, 3 -> parent=1 (u=1, v=1)
1 == 1 -> stop. LCA = 1
Binary lifting: build the table (LOG = 3)¶
up[0] = parent = [-1, 0, 0, 1, 1, 2, 4]
up[1][v] = up[0][up[0][v]]:
up[1][6] = up[0][4] = 1
up[1][3] = up[0][1] = 0
up[1][4] = up[0][1] = 0
... (overshoots become -1)
up[2][v] = up[1][up[1][v]] (mostly -1 here — tree is shallow)
Binary lifting: LCA(6, 3)¶
depth[6]=3, depth[3]=2. Lift 6 up by 3-2 = 1 step: 6 -> up[0][6] = 4.
Now u=4, v=3, both depth 2, and 4 != 3.
For k = 2,1,0:
k=1: up[1][4]=0, up[1][3]=0 -> equal, do NOT jump.
k=0: up[0][4]=1, up[0][3]=1 -> equal, do NOT jump.
Loop ends. u=4 sits one step below the LCA -> answer = up[0][4] = 1.
LCA = 1. (matches the naive result)
The rule "jump only when the ancestors differ" guarantees we stop just below the LCA — never overshooting past it.
Code Examples¶
Example A: Naive equalize-then-climb¶
Go¶
package main
import "fmt"
type Tree struct {
parent []int
depth []int
adj [][]int
}
func NewTree(n, root int, edges [][2]int) *Tree {
t := &Tree{
parent: make([]int, n),
depth: make([]int, n),
adj: make([][]int, n),
}
for _, e := range edges {
t.adj[e[0]] = append(t.adj[e[0]], e[1])
t.adj[e[1]] = append(t.adj[e[1]], e[0])
}
t.parent[root] = -1
t.dfs(root, -1, 0)
return t
}
func (t *Tree) dfs(v, p, d int) {
t.parent[v] = p
t.depth[v] = d
for _, to := range t.adj[v] {
if to != p {
t.dfs(to, v, d+1)
}
}
}
func (t *Tree) LCA(u, v int) int {
for t.depth[u] > t.depth[v] {
u = t.parent[u]
}
for t.depth[v] > t.depth[u] {
v = t.parent[v]
}
for u != v {
u = t.parent[u]
v = t.parent[v]
}
return u
}
func main() {
edges := [][2]int{{0, 1}, {0, 2}, {1, 3}, {1, 4}, {2, 5}, {4, 6}}
t := NewTree(7, 0, edges)
fmt.Println(t.LCA(6, 3)) // 1
fmt.Println(t.LCA(6, 5)) // 0
fmt.Println(t.LCA(3, 4)) // 1
}
Java¶
import java.util.*;
public class NaiveLCA {
private final int[] parent, depth;
private final List<List<Integer>> adj;
public NaiveLCA(int n, int root, int[][] edges) {
parent = new int[n];
depth = new int[n];
adj = new ArrayList<>();
for (int i = 0; i < n; i++) adj.add(new ArrayList<>());
for (int[] e : edges) {
adj.get(e[0]).add(e[1]);
adj.get(e[1]).add(e[0]);
}
dfs(root, -1, 0);
}
private void dfs(int v, int p, int d) {
parent[v] = p;
depth[v] = d;
for (int to : adj.get(v)) {
if (to != p) dfs(to, v, d + 1);
}
}
public int lca(int u, int v) {
while (depth[u] > depth[v]) u = parent[u];
while (depth[v] > depth[u]) v = parent[v];
while (u != v) { u = parent[u]; v = parent[v]; }
return u;
}
public static void main(String[] args) {
int[][] edges = {{0, 1}, {0, 2}, {1, 3}, {1, 4}, {2, 5}, {4, 6}};
NaiveLCA t = new NaiveLCA(7, 0, edges);
System.out.println(t.lca(6, 3)); // 1
System.out.println(t.lca(6, 5)); // 0
System.out.println(t.lca(3, 4)); // 1
}
}
Python¶
import sys
from typing import List, Tuple
class NaiveLCA:
def __init__(self, n: int, root: int, edges: List[Tuple[int, int]]):
self.parent = [-1] * n
self.depth = [0] * n
self.adj: List[List[int]] = [[] for _ in range(n)]
for a, b in edges:
self.adj[a].append(b)
self.adj[b].append(a)
self._dfs_iter(root)
def _dfs_iter(self, root: int) -> None:
# iterative DFS to avoid recursion-depth limits on deep trees
stack = [(root, -1, 0)]
while stack:
v, p, d = stack.pop()
self.parent[v] = p
self.depth[v] = d
for to in self.adj[v]:
if to != p:
stack.append((to, v, d + 1))
def lca(self, u: int, v: int) -> int:
while self.depth[u] > self.depth[v]:
u = self.parent[u]
while self.depth[v] > self.depth[u]:
v = self.parent[v]
while u != v:
u = self.parent[u]
v = self.parent[v]
return u
if __name__ == "__main__":
edges = [(0, 1), (0, 2), (1, 3), (1, 4), (2, 5), (4, 6)]
t = NaiveLCA(7, 0, edges)
print(t.lca(6, 3)) # 1
print(t.lca(6, 5)) # 0
print(t.lca(3, 4)) # 1
Example B: Binary lifting¶
Go¶
package main
import (
"fmt"
"math/bits"
)
type LCA struct {
n, log int
up [][]int // up[k][v] = 2^k-th ancestor of v, or -1
depth []int
adj [][]int
}
func NewLCA(n, root int, edges [][2]int) *LCA {
log := 1
for (1 << log) < n {
log++
}
l := &LCA{n: n, log: log, depth: make([]int, n), adj: make([][]int, n)}
l.up = make([][]int, log)
for k := range l.up {
l.up[k] = make([]int, n)
}
for _, e := range edges {
l.adj[e[0]] = append(l.adj[e[0]], e[1])
l.adj[e[1]] = append(l.adj[e[1]], e[0])
}
l.dfs(root, root, 0) // root's parent is itself (acts as sentinel)
for k := 1; k < log; k++ {
for v := 0; v < n; v++ {
l.up[k][v] = l.up[k-1][l.up[k-1][v]]
}
}
return l
}
func (l *LCA) dfs(v, p, d int) {
l.up[0][v] = p
l.depth[v] = d
for _, to := range l.adj[v] {
if to != p {
l.dfs(to, v, d+1)
}
}
}
// KthAncestor returns the node d steps above v, or -1 if it overshoots the root.
func (l *LCA) KthAncestor(v, d int) int {
if d > l.depth[v] {
return -1
}
for d > 0 {
k := bits.TrailingZeros(uint(d)) // lowest set bit
v = l.up[k][v]
d &= d - 1 // clear lowest set bit
}
return v
}
func (l *LCA) Query(u, v int) int {
if l.depth[u] < l.depth[v] {
u, v = v, u
}
// lift u to depth of v
u = l.KthAncestor(u, l.depth[u]-l.depth[v])
if u == v {
return u
}
for k := l.log - 1; k >= 0; k-- {
if l.up[k][u] != l.up[k][v] {
u = l.up[k][u]
v = l.up[k][v]
}
}
return l.up[0][u]
}
func main() {
edges := [][2]int{{0, 1}, {0, 2}, {1, 3}, {1, 4}, {2, 5}, {4, 6}}
l := NewLCA(7, 0, edges)
fmt.Println(l.Query(6, 3)) // 1
fmt.Println(l.Query(6, 5)) // 0
fmt.Println(l.KthAncestor(6, 2)) // 1
}
Java¶
import java.util.*;
public class BinaryLiftingLCA {
private final int n, log;
private final int[][] up; // up[k][v] = 2^k-th ancestor of v
private final int[] depth;
private final List<List<Integer>> adj;
public BinaryLiftingLCA(int n, int root, int[][] edges) {
this.n = n;
int lg = 1;
while ((1 << lg) < n) lg++;
this.log = lg;
this.up = new int[log][n];
this.depth = new int[n];
this.adj = new ArrayList<>();
for (int i = 0; i < n; i++) adj.add(new ArrayList<>());
for (int[] e : edges) {
adj.get(e[0]).add(e[1]);
adj.get(e[1]).add(e[0]);
}
dfs(root, root, 0); // root's parent is itself as sentinel
for (int k = 1; k < log; k++)
for (int v = 0; v < n; v++)
up[k][v] = up[k - 1][up[k - 1][v]];
}
private void dfs(int v, int p, int d) {
up[0][v] = p;
depth[v] = d;
for (int to : adj.get(v)) {
if (to != p) dfs(to, v, d + 1);
}
}
public int kthAncestor(int v, int d) {
if (d > depth[v]) return -1;
for (int k = 0; k < log; k++) {
if (((d >> k) & 1) == 1) v = up[k][v];
}
return v;
}
public int lca(int u, int v) {
if (depth[u] < depth[v]) { int t = u; u = v; v = t; }
u = kthAncestor(u, depth[u] - depth[v]);
if (u == v) return u;
for (int k = log - 1; k >= 0; k--) {
if (up[k][u] != up[k][v]) { u = up[k][u]; v = up[k][v]; }
}
return up[0][u];
}
public static void main(String[] args) {
int[][] edges = {{0, 1}, {0, 2}, {1, 3}, {1, 4}, {2, 5}, {4, 6}};
BinaryLiftingLCA t = new BinaryLiftingLCA(7, 0, edges);
System.out.println(t.lca(6, 3)); // 1
System.out.println(t.lca(6, 5)); // 0
System.out.println(t.kthAncestor(6, 2)); // 1
}
}
Python¶
import sys
from typing import List, Tuple
class BinaryLiftingLCA:
def __init__(self, n: int, root: int, edges: List[Tuple[int, int]]):
self.n = n
self.log = max(1, (n - 1).bit_length()) # ceil(log2(n))
self.depth = [0] * n
self.up = [[0] * n for _ in range(self.log)]
self.adj: List[List[int]] = [[] for _ in range(n)]
for a, b in edges:
self.adj[a].append(b)
self.adj[b].append(a)
self._dfs(root)
for k in range(1, self.log):
upk, upkm1 = self.up[k], self.up[k - 1]
for v in range(n):
upk[v] = upkm1[upkm1[v]]
def _dfs(self, root: int) -> None:
# iterative DFS; root's parent is itself (sentinel)
stack = [(root, root, 0)]
while stack:
v, p, d = stack.pop()
self.up[0][v] = p
self.depth[v] = d
for to in self.adj[v]:
if to != p:
stack.append((to, v, d + 1))
def kth_ancestor(self, v: int, d: int) -> int:
if d > self.depth[v]:
return -1
k = 0
while d:
if d & 1:
v = self.up[k][v]
d >>= 1
k += 1
return v
def lca(self, u: int, v: int) -> int:
if self.depth[u] < self.depth[v]:
u, v = v, u
u = self.kth_ancestor(u, self.depth[u] - self.depth[v])
if u == v:
return u
for k in range(self.log - 1, -1, -1):
if self.up[k][u] != self.up[k][v]:
u = self.up[k][u]
v = self.up[k][v]
return self.up[0][u]
if __name__ == "__main__":
edges = [(0, 1), (0, 2), (1, 3), (1, 4), (2, 5), (4, 6)]
t = BinaryLiftingLCA(7, 0, edges)
print(t.lca(6, 3)) # 1
print(t.lca(6, 5)) # 0
print(t.kth_ancestor(6, 2)) # 1
What it does: builds the 2^k-ancestor table in O(N log N), then answers LCA and k-th-ancestor queries in O(log N). Run: go run main.go | javac BinaryLiftingLCA.java && java BinaryLiftingLCA | python lca.py
Coding Patterns¶
Pattern 1: Distance between two nodes¶
The single most common use of LCA:
Because the path from u to v goes up to the LCA and back down, the two "upward" segments have lengths depth[u] − depth[lca] and depth[v] − depth[lca].
Pattern 2: "Is a an ancestor of b?"¶
a is an ancestor of b iff lifting b up to a's depth lands exactly on a:
def is_ancestor(self, a, b):
if self.depth[a] > self.depth[b]:
return False
return self.kth_ancestor(b, self.depth[b] - self.depth[a]) == a
(With an Euler-tour tin/tout you can also test this in O(1) — covered in middle.md.)
Pattern 3: k-th node on the path from u to v¶
Split at the LCA. If k ≤ depth[u] − depth[lca], the answer is the k-th ancestor of u; otherwise it is the (dist − k)-th ancestor of v. This is a frequent contest sub-task.
Error Handling¶
| Error | Cause | Fix |
|---|---|---|
| Wrong LCA on equal-depth nodes | Forgot the if u == v: return u shortcut after equalizing. | After lifting the deeper node, check equality before the big-jump loop. |
Index out of range in up[k][v] | LOG too small for the tree height. | Use LOG = ceil(log2(N)) (add 1 for safety). |
up[k][v] reads a bad sentinel | Root's parent left as 0 while node 0 is a real node. | Set root's parent to itself (up[0][root] = root) so overshooting jumps stay at the root. |
RecursionError / stack overflow in Python/Java DFS | Deep or path-like tree exceeds default recursion depth. | Use an iterative DFS (shown above) or raise the recursion limit. |
| Negative depth subtraction | Querying before building, or mixing 0- and 1-indexed depths. | Build first; pick one indexing and stick to it. |
Self query lca(u, u) returns something odd | Not handled. | It works automatically: equalizing makes them equal, returns u. |
Performance Tips¶
- Iterate the table build cache-friendly: loop
kouter,vinner, readingup[k-1]sequentially. - Use the lowest-set-bit trick for k-th ancestor to skip zero bits (
d &= d - 1). - Flatten
adjinto CSR arrays for very largeNto avoid per-node slice overhead. - Prefer iterative DFS in languages with shallow default stacks — it also avoids function-call overhead.
- If you only need distances, you do not need the Euler tour; binary lifting plus
depth[]is enough. - Choose
LOGfromN, not a fixed constant, so you never over-allocate the table for small trees.
Best Practices¶
- Decide the root once and document it; LCA is meaningless without a fixed root.
- Initialize the root's ancestor as a clean sentinel (itself or
-1) and handle it consistently everywhere. - Write the naive
O(N)LCA first and use it as an oracle to test binary lifting on random trees. - Keep
depth[],parent[], andup[][]consistent — build them in the same DFS pass. - Separate build from query so the table is computed exactly once.
- For k-th ancestor that overshoots, return a clear sentinel (
-1) rather than wrapping around.
Edge Cases & Pitfalls¶
u == v: the LCA is the node itself. The standard code handles it.- One node is an ancestor of the other: e.g.,
lca(root, x) = root. Equalize-then-climb returns the shallower node correctly. - Single-node tree:
lca(0, 0) = 0; no table rows are needed beyondup[0]. - Path (line) tree: height is
N − 1; the naive method isO(N)per query and recursion may overflow. - Disconnected input: LCA is undefined across components — validate that the input is a single tree.
- 0-indexed vs 1-indexed nodes: the most common silent bug; align your edge list and array sizes.
- Root with no parent: never read
parent[root]as a real node; use the sentinel.
Common Mistakes¶
- Sizing
LOGtoo small —2^LOGmust exceed the maximum depth, not just be near it. - Forgetting to lift the deeper node first — the big-jump loop assumes equal depth.
- Jumping when ancestors are equal in the climb loop — you must jump only when
up[k][u] != up[k][v], otherwise you overshoot past the LCA. - Returning
uinstead ofup[0][u]after the loop — the loop stops one below the LCA. - Leaving the root's parent as
0when node0is a valid node — corrupts overshooting jumps. - Recursive DFS on a deep tree — stack overflow; switch to iterative.
- Rebuilding the table per query — defeats the whole point; build once.
Cheat Sheet¶
| Task | Formula / step |
|---|---|
LOG | ceil(log2(N)) (add 1 for safety) |
| Base table | up[0][v] = parent[v] (root's parent = root) |
| Doubling | up[k][v] = up[k-1][up[k-1][v]] |
| k-th ancestor | for each set bit k of d: v = up[k][v] |
| Equalize depth | lift deeper node by |depth[u] − depth[v]| |
| Climb loop | for k = LOG-1..0: if up[k][u] != up[k][v] jump both |
| LCA answer | up[0][u] after the loop (or u if equal after equalize) |
| Distance | depth[u] + depth[v] − 2·depth[lca] |
Build complexity: O(N log N) time and space. Query: O(log N).
Visual Animation¶
See
animation.htmlfor an interactive visualization of binary-lifting LCA.The animation demonstrates: - The tree with each node's depth and
2^kjump pointers. - A query that first equalizes depth by lifting the deeper node. - The two nodes climbing together in power-of-two jumps until they sit just below the LCA. - Step / Run / Reset controls so you can watch each jump.
Summary¶
The Lowest Common Ancestor is the deepest shared ancestor of two nodes in a rooted tree. The naive equalize-then-climb method is O(N) per query and is the cleanest way to understand the problem. Binary lifting precomputes up[k][v], the 2^k-th ancestor of every node, in O(N log N), and then answers each LCA (and k-th ancestor) query in O(log N). Master the two ideas — lift the deeper node to equal depth, then jump both up by powers of two while their ancestors differ — and you have the most widely used online LCA tool in your kit. Faster O(1)-query variants (Euler tour + sparse table, and the O(N)/O(1) Farach-Colton–Bender method) appear in the higher-level files.
Further Reading¶
- Competitive Programmer's Handbook (Laaksonen), chapter on tree queries — binary lifting and Euler tour.
- Introduction to Algorithms (CLRS) — trees, DFS, and the offline LCA exercise.
- cp-algorithms.com — "Lowest Common Ancestor: Binary Lifting" and "LCA with Euler tour / Sparse Table."
- Bender & Farach-Colton, The LCA Problem Revisited (2000) — the
O(N)/O(1)reduction. - Sibling topic:
12-disjoint-set / 04-offline-lca— Tarjan's offline LCA with union-find. - Sibling topic:
14-heavy-light-decomposition— path queries that combine with LCA.