Back to Data Structures

Binary Lifting for Non-Graph Function Iteration

October 18, 2026 Wasil Zafar 23 min read

Concepts, invariants, and Python, C++, and Java implementations of Binary Lifting for Non-Graph Function Iteration

Table of Contents

  1. Why Binary Lifting for Non-Graph Function Iteration Matters
  2. Core Concepts and Terminology
  3. How Binary Lifting for Non-Graph Function Iteration Works, Step by Step
  4. Worked Example with a Full Trace
  5. Implementation in Python, C++, and Java
  6. Complexity, Correctness, and Invariants
  7. Variants, Pitfalls, and Optimizations
  8. Practice Problems and Real-World Use

Why Binary Lifting for Non-Graph Function Iteration Matters

In many algorithmic domains, we encounter a deterministic transition function $f: S \to S$ defined over a finite domain $S$ of size $N$. Given an initial state $x_0 \in S$ and a massive iteration count $K$ (where $K$ can be as large as $10^{18}$), the task is to compute the $K$-th iterated function value $f^K(x_0) = f(f(\dots f(x_0)\dots))$. A naive loop that applies $f$ sequentially $K$ times takes $O(K)$ time, which is computationally impossible when $K = 10^{18}$.

While tree-based binary lifting optimizes parent pointer navigation, Binary Lifting for Non-Graph Function Iteration applies the exact same exponential jump principle to arbitrary discrete functions, array index mappings, next-element pointers, or state machine transitions. By precomputing jump tables jump[x][j] corresponding to $f^{2^j}(x)$ for exponents $j \in [0, \lfloor \log_2 K \rfloor]$, evaluating any $K$-step functional transition reduces to combining at most $\lfloor \log_2 K \rfloor + 1$ functional jumps. This reduces the query time from $O(K)$ to $O(\log K)$ per lookup after an $O(N \log K_{max})$ preprocessing phase, while effortlessly tracking associative path monoids like path sums, minimums, or string hashes.

Binary Lifting for Non-Graph Function Iteration converts $O(K)$ sequential evaluations of a deterministic discrete function $f(x)$ into $O(\log K)$ table lookups by precomputing jump targets at exponential step sizes $2^0, 2^1, 2^2, \dots$. By expressing $K$ in its binary representation, any functional state or associated path aggregate after $K$ steps is computed in $O(\log K)$ time and $O(N \log K)$ space.

Core Concepts and Terminology

Understanding non-graph binary lifting requires formalizing functional composition over discrete state domains. Let $S = \{0, 1, \dots, N-1\}$ be a finite state space and $f: S \to S$ be a deterministic mapping.

  1. Functional Iteration ($f^k(x)$): The $k$-th recursive application of $f$, defined as $f^0(x) = x$ and $f^k(x) = f(f^{k-1}(x))$.
  2. Jump Table (jump[x][j]): A 2D array where jump[x][j] stores $f^{2^j}(x)$.
  3. Composition Recurrence: The identity $f^{2^j}(x) = f^{2^{j-1}}\!\left(f^{2^{j-1}}(x)\right)$ enables constructing column $j$ directly from column $j-1$ in $O(N)$ time.
  4. Associated Value / Path Monoid (val[x][j]): An optional auxiliary table tracking an associative property (such as the minimum, maximum, or sum of transition costs) over the $2^j$ steps starting at state $x$.
Column j doubles the reach of column j − 1 Each column of the jump table stores the state after one, two, four, or eight applications of the function and is built from the previous column in linear time. Any step count such as eleven splits into eight plus two plus one, and each set bit costs a single table lookup. Column j doubles the reach of column j − 1 LEVEL-BY-LEVEL JUMP TABLE DECOMPOSE K = 11 jump[x][0] = f(x) — 1 step jump[x][1] = f(f(x)) — 2 steps jump[x][2] — 4 steps jump[x][3] — 8 steps Each column from the one before. K = 11 in binary 1011 bit 3 set: x ← jump[x][3] (8 steps) bit 1 set: x ← jump[x][1] (2 steps) bit 0 set: x ← jump[x][0] (1 step) Eleven steps cost 3 lookups. Precomputing LOG columns takes O(N LOG) time and allows evaluating f^K(x) in O(log K) steps. jump[x][j] stores f^(2^j)(x): the state after exactly 2^j applications of f.
TermDefinitionRole in AlgorithmExample
State Space ($S$)The set of $N$ discrete domain elements over which $f$ operates.Defines domain size $N$ and array index bounds.$S = \{0, 1, 2, 3, 4, 5\}$ ($N=6$).
Base Function $f(x)$The 1-step deterministic mapping $f: S \to S$.Forms column $j=0$ in the jump table (jump[x][0] = f(x)).$f(0) = 2, f(1) = 3, f(2) = 4$.
**Jump Table jump[x][j]**Array entry storing $f^{2^j}(x)$, the state after $2^j$ steps from $x$.Enables $O(1)$ lookup for $2^j$-step function transitions.jump[0][2] stores $f^4(0)$.
**Path Monoid val[x][j]**Auxiliary table storing accumulated properties (such as path sum or min) over $2^j$ steps.Maintains path aggregates across long jumps without re-stepping.val[x][j] = val[x][j-1] + val[jump[x][j-1]][j-1].
Binary Step DecompositionRepresenting iteration count $K$ as a sum of set bits $\sum_{b_i=1} 2^i$.Dictates which powers of two are applied during a query.$K = 11 \implies 2^3 + 2^1 + 2^0$ ($8 + 2 + 1$).

How Binary Lifting for Non-Graph Function Iteration Works, Step by Step

The algorithm operates in two distinct phases: Precomputation and Query Evaluation.

Phase 1: Precomputation ($O(N \log K_{max})$)

  1. Determine $\text{LOG} = \lfloor \log_2 K_{max} \rfloor + 1$. Allocate jump[N][LOG] and optional path_sum[N][LOG].
  2. Initialize column $j = 0$: jump[x][0] = f[x] and path_sum[x][0] = weight[x] for all $x \in S$.
  3. For $j = 1$ up to $\text{LOG} - 1$: For each state $x \in S$: Let $mid = \text{jump}[x][j-1]$. Set $\text{jump}[x][j] = \text{jump}[mid][j-1]$. Set $\text{path\_sum}[x][j] = \text{path\_sum}[x][j-1] + \text{path\_sum}[mid][j-1]$.

Phase 2: Query Evaluation ($O(\log K)$)

To compute $f^K(x)$ and accumulated path weight starting from $x$:

  1. Set curr = x and acc = 0.
  2. For exponent $j = \text{LOG} - 1$ down to $0$: Check if bit $j$ of $K$ is set ((K >> j) & 1). If set: acc += path_sum[curr][j] curr = jump[curr][j]
  1. Return (curr, acc).
Query evaluation: three set bits resolve an eleven-step transition A query starts at state zero with step count eleven. The high bit adds twenty-three and moves to state four, the middle set bit adds six and moves to state two, and the low bit adds two and moves back to state four, giving final state four and total sum thirty-one. Query Evaluation: f^11(0) in Three Lookups QUERY SETUP JUMP SEQUENCE curr = 0, acc = 0, K = 11 K = 11 = 1011 in binary Scan bits j = LOG − 1 down to 0 Clear bit j = 2 is skipped for free Three lookups replace 11 steps. j = 3: acc = 0 + 23 = 23, curr = 4 j = 2: bit clear, skip j = 1: acc = 23 + 6 = 29, curr = 2 j = 0: acc = 29 + 2 = 31, curr = 4 Return (state 4, path sum 31). Applying set bits in decreasing order reaches the exact 11-step target state and path sum. Each set bit costs one table lookup; every clear bit between them costs nothing.

The control flow diagram below details the loop and decision structure for non-graph binary lifting:

Binary Lifting for Non-Graph Function Iteration control flow
flowchart TD
A[Start Query: state x, steps K] --> B[Initialize curr = x, acc = 0, j = LOG - 1]
B --> C{j >= 0?}
C -- Yes --> D{Is j-th bit of K set? K & 1<<j}
D -- Yes --> E[acc = acc + path_sum curr j]
E --> F[curr = jump curr j]
D -- No --> G[Decrement j: j = j - 1]
F --> G
G --> C
C -- No --> H[Return final state curr and path sum acc]

Worked Example with a Full Trace

Let us trace an explicit function and query end-to-end.

Domain Size: $N = 6$, States $S = \{0, 1, 2, 3, 4, 5\}$.

Base Function $f$: $f = [2, 3, 4, 1, 0, 2]$.

Transition Weights $w$: $w = [5, 10, 2, 8, 1, 4]$.

We precompute up to $j = 3$ ($2^3 = 8$ steps) and evaluate $f^{11}(0)$.

Precomputed Tables

  • jump[x][j] table:
  • $j=0$ ($2^0=1$): [2, 3, 4, 1, 0, 2]
  • $j=1$ ($2^1=2$): [4, 1, 0, 3, 2, 4]
  • $j=2$ ($2^2=4$): [2, 1, 4, 3, 0, 2]
  • $j=3$ ($2^3=8$): [4, 1, 0, 3, 2, 4]
  • path_sum[x][j] table:
  • $j=0$ ($2^0=1$): [5, 10, 2, 8, 1, 4]
  • $j=1$ ($2^1=2$): [7, 18, 3, 18, 6, 6]
  • $j=2$ ($2^2=4$): [13, 36, 10, 36, 9, 12]
  • $j=3$ ($2^3=8$): [23, 72, 19, 72, 22, 22]

Query Trace ($x = 0$, $K = 11 = 8 + 2 + 1 = 1011_2$)

  1. Bit $j=3$ ($2^3 = 8$ steps): acc += path_sum[0][3] ($0 + 23 = 23$) curr = jump[0][3] ($0 \to 4$)
  1. Bit $j=2$ ($2^2 = 4$ steps): Bit is 0, skip.
  2. Bit $j=1$ ($2^1 = 2$ steps): acc += path_sum[4][1] ($23 + 6 = 29$) curr = jump[4][1] ($4 \to 2$)
  1. Bit $j=0$ ($2^0 = 1$ step): acc += path_sum[2][0] ($29 + 2 = 31$) curr = jump[2][0] ($2 \to 4$)

Final Answer for $f^{11}(0)$: Final State = 4, Total Path Sum = 31.

Precomputed jump and path_sum tables for f = [2, 3, 4, 1, 0, 2] Two tables over six states. The jump table stores the destination state after one, two, four, and eight steps, and the path sum table stores the accumulated weight over the same steps. Eleven steps from state zero end at state four with total sum thirty-one. Precomputed jump and path_sum tables JUMP TABLE jump[x][j] PATH SUMS path_sum[x][j] x j=0 (1) j=1 (2) j=2 (4) j=3 (8) 0 2 4 2 4 1 3 1 1 1 2 4 0 4 0 3 1 3 3 3 4 0 2 0 2 5 2 4 2 4 Column j stores the state after 2^j steps of f, built from column j − 1. jump[0][3] = 4: eight steps from 0. x j=0 (1) j=1 (2) j=2 (4) j=3 (8) 0 5 7 13 23 1 10 18 36 72 2 2 3 10 19 3 8 18 36 72 4 1 6 9 22 5 4 6 12 22 Column j accumulates weights over the same 2^j steps as the jump table. path_sum[0][3] = 23 for eight steps. All values match manual step simulation: 11 steps from 0 gives state 4 and path sum 31. jump stores destinations; path_sum stores the weight accumulated over the same jumps.
Worked Example

Trace It by Hand

Trace Challenge: Using the precomputed tables above:

  1. Evaluate $f^{13}(1)$ with initial weight sum starting from state $x = 1$. ($13 = 8 + 4 + 1$).
  2. Evaluate $f^{7}(3)$ starting from state $x = 3$. ($7 = 4 + 2 + 1$).

Questions:

  1. For $f^{13}(1)$, what is the intermediate state and path sum after applying bit $j=3$ ($2^3 = 8$ steps)?
  2. What is the final state and total path sum for $f^{13}(1)$?
  3. What is the final state and total path sum for $f^{7}(3)$?

Answer Verification:

  1. Starting at $x=1$, bit $j=3$ takes $8$ steps: acc += path_sum[1][3] ($72$), curr = jump[1][3] ($1$).
  2. Bit $j=2$ ($4$ steps): acc += 36 ($108$), curr = 1. Bit $j=0$ ($1$ step): acc += 10 ($118$), curr = 3. Final result: State 3, Path Sum 118.
  3. For $f^7(3)$: $7 = 4 + 2 + 1$. Bit $j=2$: acc = 36, curr = 3. Bit $j=1$: acc = 36 + 18 = 54, curr = 3. Bit $j=0$: acc = 54 + 8 = 62, curr = 1. Final result: State 1, Path Sum 62.

Implementation in Python, C++, and Java

The complete, runnable code blocks below demonstrate Non-Graph Binary Lifting with path sum accumulation. All three language implementations execute identical logic, use the exact same sample dataset, and print identical output.

class BinaryLiftingFunction:
    def __init__(self, f, weights, max_k):
        self.n = len(f)
        self.LOG = max_k.bit_length()
        if self.LOG == 0:
            self.LOG = 1

        self.jump = [[0] * self.LOG for _ in range(self.n)]
        self.path_sum = [[0] * self.LOG for _ in range(self.n)]

        # Initialize base column (j = 0, 2^0 = 1 step)
        for i in range(self.n):
            self.jump[i][0] = f[i]
            self.path_sum[i][0] = weights[i]

        # Build power-of-two jump tables
        for j in range(1, self.LOG):
            for i in range(self.n):
                mid = self.jump[i][j - 1]
                self.jump[i][j] = self.jump[mid][j - 1]
                self.path_sum[i][j] = self.path_sum[i][j - 1] + self.path_sum[mid][j - 1]

    def query(self, start_state, k):
        curr = start_state
        total_sum = 0
        for j in range(self.LOG - 1, -1, -1):
            if (k >> j) & 1:
                total_sum += self.path_sum[curr][j]
                curr = self.jump[curr][j]
        return curr, total_sum


if __name__ == "__main__":
    f = [2, 3, 4, 1, 0, 2]
    weights = [5, 10, 2, 8, 1, 4]
    max_k = 100

    bl = BinaryLiftingFunction(f, weights, max_k)

    queries = [(0, 11), (1, 13), (3, 7)]
    for idx, (x, k) in enumerate(queries, 1):
        end_state, sum_val = bl.query(x, k)
        print(f"Query {idx} (start={x}, K={k}): Final State = {end_state}, Path Sum = {sum_val}")
#include <iostream>
#include <vector>
#include <utility>

class BinaryLiftingFunction {
private:
    int n;
    int LOG;
    std::vector<std::vector<int>> jump;
    std::vector<std::vector<long long>> path_sum;

public:
    BinaryLiftingFunction(const std::vector<int>& f, const std::vector<long long>& weights, long long max_k) {
        n = static_cast<int>(f.size());
        LOG = 0;
        while ((1LL << LOG) <= max_k) {
            LOG++;
        }
        if (LOG == 0) LOG = 1;

        jump.assign(n, std::vector<int>(LOG, 0));
        path_sum.assign(n, std::vector<long long>(LOG, 0));

        // Initialize base column (j = 0, 2^0 = 1 step)
        for (int i = 0; i < n; ++i) {
            jump[i][0] = f[i];
            path_sum[i][0] = weights[i];
        }

        // Build power-of-two jump tables
        for (int j = 1; j < LOG; ++j) {
            for (int i = 0; i < n; ++i) {
                int mid = jump[i][j - 1];
                jump[i][j] = jump[mid][j - 1];
                path_sum[i][j] = path_sum[i][j - 1] + path_sum[mid][j - 1];
            }
        }
    }

    std::pair<int, long long> query(int start_state, long long k) const {
        int curr = start_state;
        long long total_sum = 0;
        for (int j = LOG - 1; j >= 0; --j) {
            if ((k >> j) & 1LL) {
                total_sum += path_sum[curr][j];
                curr = jump[curr][j];
            }
        }
        return {curr, total_sum};
    }
};

int main() {
    std::vector<int> f = {2, 3, 4, 1, 0, 2};
    std::vector<long long> weights = {5, 10, 2, 8, 1, 4};
    long long max_k = 100;

    BinaryLiftingFunction bl(f, weights, max_k);

    std::vector<std::pair<int, long long>> queries = {{0, 11}, {1, 13}, {3, 7}};
    for (size_t i = 0; i < queries.size(); ++i) {
        auto res = bl.query(queries[i].first, queries[i].second);
        std::cout << "Query " << (i + 1) << " (start=" << queries[i].first
                  << ", K=" << queries[i].second << "): Final State = "
                  << res.first << ", Path Sum = " << res.second << "\n";
    }

    return 0;
}
public class Main {

    public static class BinaryLiftingFunction {
        private final int n;
        private final int LOG;
        private final int[][] jump;
        private final long[][] pathSum;

        public BinaryLiftingFunction(int[] f, long[] weights, long maxK) {
            this.n = f.length;
            int logCount = 0;
            while ((1L << logCount) <= maxK) {
                logCount++;
            }
            this.LOG = Math.max(1, logCount);

            this.jump = new int[n][LOG];
            this.pathSum = new long[n][LOG];

            // Initialize base column (j = 0, 2^0 = 1 step)
            for (int i = 0; i < n; i++) {
                jump[i][0] = f[i];
                pathSum[i][0] = weights[i];
            }

            // Build power-of-two jump tables
            for (int j = 1; j < LOG; j++) {
                for (int i = 0; i < n; i++) {
                    int mid = jump[i][j - 1];
                    jump[i][j] = jump[mid][j - 1];
                    pathSum[i][j] = pathSum[i][j - 1] + pathSum[mid][j - 1];
                }
            }
        }

        public long[] query(int startState, long k) {
            int curr = startState;
            long totalSum = 0;
            for (int j = LOG - 1; j >= 0; j--) {
                if (((k >> j) & 1L) == 1L) {
                    totalSum += pathSum[curr][j];
                    curr = jump[curr][j];
                }
            }
            return new long[]{curr, totalSum};
        }
    }

    public static void main(String[] args) {
        int[] f = {2, 3, 4, 1, 0, 2};
        long[] weights = {5, 10, 2, 8, 1, 4};
        long maxK = 100;

        BinaryLiftingFunction bl = new BinaryLiftingFunction(f, weights, maxK);

        long[][] queries = {{0, 11}, {1, 13}, {3, 7}};
        for (int i = 0; i < queries.length; i++) {
            int start = (int) queries[i][0];
            long k = queries[i][1];
            long[] res = bl.query(start, k);
            System.out.println("Query " + (i + 1) + " (start=" + start + ", K=" + k +
                    "): Final State = " + res[0] + ", Path Sum = " + res[1]);
        }
    }
}

Complexity, Correctness, and Invariants

The formal justification for Non-Graph Binary Lifting relies on the associativity of functional composition and the uniqueness of binary number representation.

Strategy / AlgorithmPreprocessing TimeQuery TimeAuxiliary SpaceKey Trade-offs / Limitations
Binary Lifting (Non-Graph)$O(N \log K_{max})$$O(\log K)$$O(N \log K_{max})$Optimal for multiple queries on static functions; easily tracks path monoids.
Naive Loop Iteration$O(1)$$O(K)$$O(1)$Unusable for $K \ge 10^9$; extremely simple implementation.
Floyd's Cycle Finding$O(N)$$O(N + K \bmod C)$$O(1)$$O(N)$ query time to walk tail; does not naturally track general path monoids.
Matrix Exponentiation$O(N^3 \log K)$$O(N^3 \log K)$$O(N^2)$Designed for linear state recurrences ($f(x) = M \cdot x$); slow when $N \ge 10^3$.

Variants, Pitfalls, and Optimizations

Common Variants

  1. Binary Lifting over Monoids (Path Min / Max / GCD / Hash):

When each state transition $x \to f(x)$ carries an associative value, path aggregates are precomputed alongside jump pointers: val[x][j] = val[x][j-1] ⊕ val[jump[x][j-1]][j-1], where $\oplus$ is any associative operator. 2. Double Binary Lifting (Binary Search on Step Count): Used to find the minimum steps $K$ required to satisfy a target condition (such as path sum $\ge T$). By trying exponents $j = \text{LOG}-1$ down to $0$, if taking a $2^j$-step jump keeps the total path value below $T$, the search jumps $2^j$ steps and adds $2^j$ to $K$. 3. Partial Function Handling (Terminal States): If $f(x)$ is a partial mapping where some states have no outgoing transition, set terminal states to self-loops ($f(x) = x$) with identity transition weight ($0$ for sum, $\infty$ for min) to prevent pointer errors during binary lifting.

Read the path value before advancing the pointer The correct order adds the stored path value for the current state and only then advances the pointer. The buggy order advances the pointer first and reads the value at the new state, counting the wrong segment for every set bit and corrupting the accumulated total. Accumulate first, advance the pointer second CORRECT: ACCUMULATE FIRST BUG: ADVANCE FIRST acc += path_sum[curr][j] curr = jump[curr][j] Value read at the pre-jump state Covers exactly the 2^j jumped steps Every set bit sums the right segment. curr = jump[curr][j] acc += path_sum[curr][j] Value read at the post-jump state Wrong segment counted for every bit Path sums are silently corrupted. Always update path values using the current state before advancing the current pointer. acc must read path_sum[curr][j] first; only then may curr move to jump[curr][j].

Top Implementation Pitfalls:

  1. Non-Commutative Monoid Order Bug: When combining non-commutative properties (such as matrix multiplication or string concatenation), applying combine(val[curr][j], acc) instead of combine(acc, val[curr][j]) reverses operand order and produces corrupted results.
  2. Off-by-One Bit Length: Defining $\text{LOG}$ such that $2^{\text{LOG}-1} \lt K_{max}$ truncates high-order set bits, causing queries with large $K$ to take insufficient steps. Always ensure $2^{\text{LOG}} > K_{max}$.
  3. Partial Functions Without Self-Loops: Failing to define $f(x) = x$ for terminal states causes array index out-of-bounds exceptions during binary lifting table construction.

Practice Problems and Real-World Use

Binary lifting for non-graph function iteration is a core technique across competitive programming, string processing, state machine fast-forwarding, and distributed lookup systems.

Problem / ApplicationCore Concept TestedDifficulty Label
Codeforces 1175E - Minimal Segment CoverBinary search on step count using functional jump tables over interval endpoints.Hard
AtCoder ABC241E - Cooking SteaksBinary lifting on index remainder function $f(x) = (x + A_{x \bmod N}) \bmod N$ with path sum.Medium-Hard
LeetCode 1483 - Kth Ancestor of a Tree NodeBinary lifting jump table construction over array index transitions.Hard
Suffix Automaton Link HoppingFast-forwarding state matching along suffix link paths ($f(\text{state}) = \text{link}[\text{state}]$).Very Hard
DFA / State Machine Fast-ForwardingSkipping $10^{18}$ cycle steps in deterministic finite automata for long inputs.Real-World System
Distributed Hash Table (Chord Routing)Finger tables maintain $2^i$-step jumps in circular identifier spaces for $O(\log N)$ routing.Real-World System
Jump Games / Next Greater ElementPrecomputing $2^j$ hops over next greater element pointers ($f(i) = \text{next\_greater}[i]$).Medium-Hard