Skip to content

Binary Lifting Search

Find a target state by jumping through powers of two in a precomputed lifting table.

Binary lifting search finds a target ancestor, state, or boundary by jumping in powers of two. It is common on trees, functional graphs, and ordered implicit structures where repeated “next” or “parent” moves are needed.

The method preprocesses jumps of length:

1,2,4,8, 1, 2, 4, 8, \dots

Then a query combines these jumps to move quickly.

Problem

Given a directed parent relation parent[v], preprocess a table so that queries can find the kk-th ancestor of a node vv.

More generally, binary lifting can search for the farthest reachable state that satisfies a condition.

Key Idea

Define:

up[v][j] up[v][j]

as the ancestor of node vv after 2j2^j parent steps.

Then:

up[v][0]=parent[v] up[v][0] = parent[v]

and

up[v][j]=up[up[v][j1]][j1] up[v][j] = up[up[v][j-1]][j-1]

This recurrence builds large jumps from smaller jumps.

Preprocessing

build_binary_lifting(parent):
    n = length(parent)
    LOG = ceil(log2(n)) + 1

    up = table of size n by LOG

    for v from 0 to n - 1:
        up[v][0] = parent[v]

    for j from 1 to LOG - 1:
        for v from 0 to n - 1:
            if up[v][j - 1] == -1:
                up[v][j] = -1
            else:
                up[v][j] = up[up[v][j - 1]][j - 1]

    return up

Query: K-th Ancestor

Use the binary representation of kk. If bit jj is set, jump by 2j2^j.

kth_ancestor(up, v, k):
    j = 0

    while k > 0 and v != -1:
        if k & 1:
            v = up[v][j]
        k = k >> 1
        j = j + 1

    return v

Example

Suppose the parent chain is:

01234 0 \leftarrow 1 \leftarrow 2 \leftarrow 3 \leftarrow 4

Here:

nodeparent
0-1
10
21
32
43

The 33-rd ancestor of node 44 is node 11:

4321 4 \to 3 \to 2 \to 1

Since:

3=2+1 3 = 2 + 1

the query uses one jump of length 22 and one jump of length 11.

Search Form

Binary lifting can also find the highest ancestor satisfying a predicate.

highest_ancestor_while(up, v, ok):
    for j from LOG - 1 down to 0:
        u = up[v][j]
        if u != -1 and ok(u):
            v = u

    return v

This is useful when ok remains true while moving upward, then becomes false beyond a boundary.

Correctness

The preprocessing recurrence is correct because a jump of length 2j2^j can be decomposed into two jumps of length 2j12^{j-1}.

During a query, the binary representation of kk decomposes the requested distance into powers of two. Each selected jump moves exactly that distance. Therefore the final node is the kk-th ancestor, or -1 if the chain ends.

For predicate search, the algorithm tests larger jumps first. It keeps only jumps that preserve the predicate, so it never crosses the boundary. When no larger valid jump remains, the current node is the farthest reachable valid state.

Complexity

Let nn be the number of nodes and let:

L=log2n L = \lceil \log_2 n \rceil
operationtimespace
preprocessingO(nL)O(nL)O(nL)O(nL)
k-th ancestor queryO(L)O(L)O(1)O(1)
predicate searchO(L)O(L)O(1)O(1)

Use Cases

  • K-th ancestor in a tree
  • Lowest common ancestor
  • Jump pointers in functional graphs
  • Farthest valid ancestor
  • Searching over repeated transitions

Notes

  • Binary lifting requires static or mostly static structure.
  • Updates are expensive if many parent links change.
  • It is best when many queries justify preprocessing.
  • The same idea applies to sparse tables and doubling DP.

Implementation

def build_binary_lifting(parent):
    n = len(parent)
    log = max(1, n.bit_length())
    up = [[-1] * log for _ in range(n)]

    for v in range(n):
        up[v][0] = parent[v]

    for j in range(1, log):
        for v in range(n):
            u = up[v][j - 1]
            up[v][j] = -1 if u == -1 else up[u][j - 1]

    return up

def kth_ancestor(up, v, k):
    j = 0
    while k > 0 and v != -1:
        if k & 1:
            if j >= len(up[v]):
                return -1
            v = up[v][j]
        k >>= 1
        j += 1
    return v
func BuildBinaryLifting(parent []int) [][]int {
    n := len(parent)
    log := 1
    for (1 << log) <= n {
        log++
    }

    up := make([][]int, n)
    for v := 0; v < n; v++ {
        up[v] = make([]int, log)
        up[v][0] = parent[v]
    }

    for j := 1; j < log; j++ {
        for v := 0; v < n; v++ {
            u := up[v][j-1]
            if u == -1 {
                up[v][j] = -1
            } else {
                up[v][j] = up[u][j-1]
            }
        }
    }

    return up
}

func KthAncestor(up [][]int, v int, k int) int {
    j := 0
    for k > 0 && v != -1 {
        if k&1 == 1 {
            if j >= len(up[v]) {
                return -1
            }
            v = up[v][j]
        }
        k >>= 1
        j++
    }
    return v
}