Skip to content
CCC Python Course

LCA and binary lifting on trees

Module
M7.13
Lesson
1 of 1
Reading time
4 min

In this lesson

  • Find the lowest common ancestor of two nodes in a tree using binary lifting.
  • Build a sparse table to precompute ancestors at powers-of-two distances.
  • Answer range queries on a tree by lifting up the tree logarithmically.
  • Extend binary lifting to arbitrary weights and node labels.

Two nodes in a tree share a common ancestor: their lowest common ancestor (LCA) is the deepest node that lies on the path between them. Finding the LCA is useful for computing distances, comparing node relationships, and solving path queries on trees.

A naive approach walks both nodes up to the root until you find their common ancestor. On a deep tree, this is slow. Binary lifting uses preprocessing to answer LCA queries logarithmically.

The idea: precomputed jumps

Store ancestors at increasing distances: each node remembers its parent, its grandparent (parent of parent), its 4th ancestor, its 8th ancestor, and so on. These are ancestors at distances 1, 2, 4, 8, 16, ... which are powers of two.

When you want to find the LCA of two nodes, first make sure they are at the same depth. If not, jump the deeper node up to match the shallower one. Then jump both nodes up together in decreasing order of jump sizes until they meet.

Preprocessing: building the sparse table

The table has rows for nodes and columns for jump sizes (log scale). ancestors[u][k] stores the 2^k-th ancestor of node u.

Base case: ancestors[u][0] is the parent of u.

Recurrence: ancestors[u][k] is the 2^(k-1)-th ancestor of ancestors[u][k-1]. In code, ancestors[u][k] = ancestors[ancestors[u][k-1]][k-1].

examples/lca_preprocess.py
import mathimport sys

def main() -> None:    data = sys.stdin.read().split()    n = int(data[0])
    # Build adjacency list    adj = [[] for _ in range(n)]    for i in range(1, n):        u = int(data[2 * (i - 1) + 1])        v = int(data[2 * (i - 1) + 2])        adj[u].append(v)        adj[v].append(u)
    # DFS to compute depth    depth = [-1] * n    parent = [-1] * n
    def dfs(u, p, d):        depth[u] = d        parent[u] = p        for v in adj[u]:            if v != p:                dfs(v, u, d + 1)
    dfs(0, -1, 0)
    # Build sparse table    LOG = math.ceil(math.log2(n)) + 1    ancestors = [[-1] * LOG for _ in range(n)]
    for u in range(n):        ancestors[u][0] = parent[u]
    for k in range(1, LOG):        for u in range(n):            if ancestors[u][k - 1] != -1:                ancestors[u][k] = ancestors[ancestors[u][k - 1]][k - 1]
    # Output sparse table    output = []    for u in range(n):        row = [str(ancestors[u][k]) for k in range(LOG)]        output.append(" ".join(row))
    sys.stdout.write("\n".join(output) + "\n")
if __name__ == "__main__":    main()

Input

5
0 1
0 2
2 3
2 4

Output

-1 -1 -1 -1
0 -1 -1 -1
0 -1 -1 -1
2 0 -1 -1
2 0 -1 -1
Build the sparse table for binary lifting

The table uses O(N log N) space, where N is the number of nodes. Building it takes O(N log N) time.

Querying: finding the LCA

To find the LCA of u and v:

  1. If depth[u] < depth[v], swap them. Now u is deeper.
  2. Bring u up to match v's depth by jumping in decreasing powers of 2.
  3. If u == v, return u. Otherwise, lift both nodes up simultaneously until their parents are the same.
examples/lca_query.py
import mathimport sys

def main() -> None:    data = sys.stdin.read().split()    idx = 0    n = int(data[idx])    idx += 1
    # Build tree    adj = [[] for _ in range(n)]    for _ in range(n - 1):        u = int(data[idx])        v = int(data[idx + 1])        idx += 2        adj[u].append(v)        adj[v].append(u)
    depth = [-1] * n    parent = [-1] * n
    def dfs(u, p, d):        depth[u] = d        parent[u] = p        for v in adj[u]:            if v != p:                dfs(v, u, d + 1)
    dfs(0, -1, 0)
    LOG = math.ceil(math.log2(n)) + 1    ancestors = [[-1] * LOG for _ in range(n)]
    for u in range(n):        ancestors[u][0] = parent[u]
    for k in range(1, LOG):        for u in range(n):            if ancestors[u][k - 1] != -1:                ancestors[u][k] = ancestors[ancestors[u][k - 1]][k - 1]
    def lca(u, v):        if depth[u] < depth[v]:            u, v = v, u        diff = depth[u] - depth[v]        for k in range(LOG):            if (diff >> k) & 1:                u = ancestors[u][k]        if u == v:            return u        for k in range(LOG - 1, -1, -1):            if ancestors[u][k] != ancestors[v][k]:                u = ancestors[u][k]                v = ancestors[v][k]        return ancestors[u][0]
    q = int(data[idx])    idx += 1    output = []    for _ in range(q):        a = int(data[idx])        b = int(data[idx + 1])        idx += 2        output.append(str(lca(a, b)))
    sys.stdout.write("\n".join(output) + "\n")
if __name__ == "__main__":    main()

Input

5
0 1
0 2
2 3
2 4
3
3 4
1 4
0 4

Output

2
0
0
Query the LCA of two nodes in logarithmic time

Each query takes O(log N) time because you make at most log N jumps.

A worked example

Consider a tree with 5 nodes and edges: 0–1, 0–2, 2–3, 2–4.

    0   / \  1   2     / \    3   4

The sparse table:

ancestors[u][k]  k=0  k=1  k=2  k=3u=0               -1   -1   -1   -1u=1                0   -1   -1   -1u=2                0   -1   -1   -1u=3                2    0   -1   -1u=4                2    0   -1   -1

(Where -1 means no ancestor exists.)

To find LCA(3, 4): both are at depth 2. Lift node 3 with k=0 to jump 1 level: it goes to node 2. Lift node 4 with k=0 to jump 1 level: it goes to node 2. They are the same, so LCA = 2.

To find LCA(1, 4): node 1 is at depth 1, node 4 at depth 2. Lift node 4 to depth 1 with k=0: it goes to node 2. Now both are at depth 1. Check if they are equal: no. Lift both with k=0: node 1 goes to node 0, node 2 goes to node 0. Their parents are the same, so LCA = 0.

Common mistakes

Mistake 1: Forgetting to swap. If you do not ensure the deeper node is u before lifting, you will lift the wrong node. Always check depth[u] and swap if needed.

Mistake 2: Off-by-one in the depth difference. If depth[u] = 3 and depth[v] = 1, the difference is 2. You need to lift u by 2 levels. Use diff = depth[u] - depth[v] and then jump by powers of 2 within that budget. If (diff >> k) & 1 is true, you jump by 2^k levels.

Mistake 3: Forgetting to check if u == v after equalizing depth. If the two nodes are the same after bringing them to the same depth, return immediately. Do not lift further.

Mistake 4: Indexing ancestors wrongly. Remember that ancestors[u][k] is the 2^k-th ancestor. To jump 1 level, use k=0. To jump 2 levels, use k=1. Be careful with the power-of-two relationship.

A second worked example

Consider a deeper tree with 7 nodes:

       0      / \     1   2    / \   \   3   4   5        \         6

Edges: 0-1, 0-2, 1-3, 1-4, 2-5, 4-6. Depths: [0, 1, 1, 2, 2, 2, 3].

To find LCA(3, 6): node 3 is at depth 2, node 6 is at depth 3. Lift node 6 by 1 level (k=0): node 6 goes to node 4. Now both are at depth 2. Are they equal? No. Lift both by powers of 2, starting from the highest k. With k=1, we would jump 2 levels, but we are already at depth 2 and need to stay at depth ≥ 1, so we check if ancestors[3][1] and ancestors[4][1] are different. Node 3's 2nd ancestor is node 0 (through 1). Node 4's 2nd ancestor is node 0 (through 1). They are the same, so we do not jump. The LCA is ancestors[3][0] = 1.

Extending binary lifting

The same approach works with arbitrary edge weights. Instead of just tracking ancestors, also track the minimum weight (or maximum, or sum) along the path to each ancestor.

On a tree with node labels, store the label at each ancestor. This allows computing properties of paths: "What is the maximum label on the path from u to v?"

You can also use binary lifting on DAGs (directed acyclic graphs) if you precompute the ancestors in topological order.

Recap

Binary lifting preprocesses the tree to support O(log N) LCA queries and path queries. Build a sparse table of 2^k-th ancestors for each node, then answer queries by jumping in powers of two. Ensure depths are equal before querying, and use bitwise operations to extract the binary representation of jump distances. This technique extends to weighted paths, node labels, and DAG problems.