Skip to content
CCC Python Course

Doubling and binary lifting on sequences

Module
M6.12
Lesson
1 of 1
Reading time
5 min

In this lesson

  • Build and query a binary-lifting table to jump O(log n) steps in a sequence.
  • Apply binary lifting to find ancestors in trees and reachable nodes in a graph.
  • Optimize queries by precomputing powers of two.

In a linked structure, reaching a node far away by following one step at a time costs as many steps as the distance. Jumping 1,000 steps forward one at a time takes 1,000 steps. Binary lifting precomputes where you land after jumping 1, 2, 4, 8, 16, and so on steps, so any jump, however far, is reached by combining a handful of precomputed jumps in O(log⁡n)O(\log n) time.

The table lift[node][k] stores which node you reach after jumping 2k2^k steps from node. Any jump of d steps decomposes into its binary representation: jumping 13 steps means jumping 8, then 4, then 1, since 13 = 8 + 4 + 1. Each of those three jumps is already precomputed, so the whole distance is covered in three lookups instead of thirteen steps.

Building the table

For each node, the table stores O(log⁡n)O(\log n) entries: where that node lands after 1, 2, 4, 8, ... steps. The base case is lift[node][0], the node reached after exactly one step, whether that is the next node in a sequence or the parent in a tree.

Every later entry follows from a recurrence: lift[node][k] = lift[lift[node][k - 1]][k - 1]. Jumping 2k2^k steps from node is the same as jumping 2k−12^{k-1} steps to reach an intermediate node, then jumping 2k−12^{k-1} steps again from there, since 2k−1+2k−1=2k2^{k-1} + 2^{k-1} = 2^k. Filling the table column by column, smaller powers first, takes O(nlog⁡n)O(n \log n) time in total, since each of the O(log⁡n)O(\log n) columns takes O(n)O(n) to fill.

Querying with the table

To jump d steps from a starting node, look at the binary representation of d bit by bit. Whenever a bit is set, make the corresponding precomputed jump and move to the resulting node. Once every bit has been checked, you have reached the destination.

To jump 13 steps, check bit 0 (13 & 1 = 1, so jump 20=12^0 = 1), bit 1 (13 & 2 = 2, so jump 21=22^1 = 2), and bit 2 (13 & 4 = 4, so jump 22=42^2 = 4). Each jump is a single table lookup, so the whole query costs O(log⁡d)O(\log d).

A linked sequence

Take a linked list where node i points to node i + 1, and you want to find the node 17 steps ahead of node 0.

examples/binary_lifting.py
import sys

def main() -> None:    data = sys.stdin.read().split()    if not data:        return
    n = int(data[0])    start = int(data[1])    target_jump = int(data[2])
    # Build a simple linked list: node i -> node i+1    # lift[node][k] = node reached after 2^k steps
    # Determine table size needed    LOG = 20    lift = [[-1] * LOG for _ in range(n)]
    # Base case: lift[i][0] = i+1 (one step ahead)    for i in range(n - 1):        lift[i][0] = i + 1
    # Fill the table: lift[i][k] = where you reach from lift[i][k-1] after 2^(k-1) steps    for k in range(1, LOG):        for i in range(n):            if lift[i][k - 1] != -1:                lift[i][k] = lift[lift[i][k - 1]][k - 1]
    # Query: jump target_jump steps from start    current = start    remaining = target_jump
    for k in range(LOG - 1, -1, -1):        if remaining >= (1 << k):            if lift[current][k] == -1:                break            current = lift[current][k]            remaining -= (1 << k)
    if remaining == 0:        print(f"After jumping {target_jump} steps from {start}, reach node {current}")    else:        print(f"Cannot jump {target_jump} steps from {start} (only {target_jump - remaining} steps possible)")

if __name__ == "__main__":    main()

Input

20 0 17

Output

After jumping 17 steps from 0, reach node 17
Jumping through a linked sequence with a precomputed table

Since the list is a straight chain, jumping 17 steps from node 0 always lands on node 17, which the program confirms after building the table and decomposing 17 into powers of two. The value of the table shows up on a less predictable structure, such as a tree, where "17 steps from here" is not simply "add 17".

To see the table in action, build it for a chain of nodes 0 through 19. lift[i][0] is i + 1 for every node. lift[i][1] composes two single steps: lift[0][1] = lift[lift[0][0]][0] = lift[1][0] = 2. Later columns compose in the same way. To jump 13 steps from node 0, decompose 13 as 8 + 4 + 1. lift[0][3] = 8 reaches node 8, lift[8][2] = 12 reaches node 12 from there, and lift[12][0] = 13 reaches node 13 last. Three table lookups replace thirteen single steps.

Handling nodes with no successor

Some sequences end, or a node may have no valid next step. Store a sentinel value, such as -1, at any table entry that would otherwise point past the end, and check for it before making a jump. If a jump would land on that sentinel, the destination is unreachable, and the query should stop there instead of continuing with an invalid index.

Why the composition is correct, and its cost

Every non-negative integer has a unique binary representation, so any distance can be written as a sum of distinct powers of two. Precomputing jumps of size 1, 2, 4, 8, and so on lets you compose any of those sums. The recurrence works because a jump of 2k−12^{k-1} steps behaves the same way no matter which node it starts from: reaching the 2k2^k-step destination is exactly reaching the 2k−12^{k-1}-step destination, then jumping another 2k−12^{k-1} steps from there.

Building the table costs O(nlog⁡n)O(n \log n): one entry per node per power of two, with O(log⁡n)O(\log n) powers of two needed to cover a sequence of length nn. Storing the table costs the same, O(nlog⁡n)O(n \log n) space. Once it exists, a single query costs O(log⁡d)O(\log d), far below the O(d)O(d) a naive step-by-step walk would need, which matters when the same structure answers many queries.

Ancestors in a tree

The same table works on tree parent pointers instead of a linear chain. Let lift[node][p] be the node's 2p2^p-th ancestor, with the base case lift[node][0] = parent[node] and the same recurrence, lift[node][p] = lift[lift[node][p - 1]][p - 1].

To find the 50th ancestor of a node, decompose 50 as 32 + 16 + 2. lift[node][5] jumps 32 levels up, lift[result][4] jumps 16 more, and lift[result][1] jumps the last 2. The whole query costs O(log⁡k)O(\log k), fast even when kk is as large as 10610^6.

Common mistakes

Confusing the table index with the jump size is the most common error. Column k holds jumps of size 2k2^k, not kk itself, and lift[node][0] must be the immediate next node, not the node itself. Check the base case carefully before trusting anything built from it.

Forgetting a boundary condition is another. Jumping past the end of a sequence, or above the root of a tree, needs a sentinel value such as -1 to catch it. Without one, a query either reads past the end of the table or loops on invalid data.

A third mistake is writing the wrong recurrence. lift[i][k] = lift[i][k - 1] + 1 looks plausible but only adds one step regardless of k; the correct form, lift[i][k] = lift[lift[i][k - 1]][k - 1], composes two jumps of the previous size. Mixing these up produces a table that runs without error but answers every query wrong.

Recap

Binary lifting precomputes jumps of 2k2^k steps so that any jump of dd steps is answered in O(log⁡d)O(\log d) time, by decomposing dd into powers of two. Building the table costs O(nlog⁡n)O(n \log n) time and space, after which each query is fast. The same table structure answers "jump forward in a sequence" and "find an ancestor in a tree", since both are really the same recurrence applied to a different notion of "next node".

Practice

Try these on the judges. Each link opens the problem on WMOJ or DMOJ.

  1. 2022 S3
    Good Samples (opens on WMOJ in a new tab) WMOJ

    Follow a queue of linked instructions to find the first one that fires.

  2. 2019 S3
    Arithmetic Square (opens on DMOJ in a new tab) DMOJ

    Find the maximum value on the path between two nodes in a tree.

    Why DMOJ: An older tree-query problem that still makes good practice for this module.