Algorithm Search⌘K

Ladder decomposition in Python

Answer level ancestor queries in O(1) with long-path ladders and jump pointers.

File
ladder_decomposition.py, 105 lines
Level
I0 not checked yet
Toolchain
Python 3.14, pytest 9
Test framework
pytest
Run the tests
python3 -m pytest
ladder_decomposition.pyRawGitHub
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/ladder-decomposition/. The authors are listed in prov/VJP1A9.jsonl.

from collections import deque


class LadderLevelAncestor:
    """Level ancestor queries by jump pointers and ladders.

    The tree has the vertices 0 .. n-1 (n >= 1), is unweighted, and is given
    by its n - 1 undirected edges and a root. Preprocessing takes
    O(n log n) time and memory (the ladders alone take O(n)); each query
    takes O(1) time. No recursion is used.
    """

    def __init__(self, n: int, edges: list[tuple[int, int]], root: int = 0) -> None:
        if n < 1 or len(edges) != n - 1 or not 0 <= root < n:
            raise ValueError("a tree needs n >= 1 vertices, n - 1 edges and a root in range")
        adj: list[list[int]] = [[] for _ in range(n)]
        for a, b in edges:
            if not (0 <= a < n and 0 <= b < n) or a == b:
                raise ValueError("edge endpoints must be distinct vertices in range")
            adj[a].append(b)
            adj[b].append(a)
        parent = [-1] * n
        depth = [-1] * n
        depth[root] = 0
        order = [root]
        queue = deque([root])
        while queue:  # @line L1
            v = queue.popleft()
            for w in adj[v]:
                if depth[w] == -1:
                    depth[w] = depth[v] + 1
                    parent[w] = v
                    order.append(w)
                    queue.append(w)
        if len(order) != n:
            raise ValueError("the edges do not form a tree")
        height = [1] * n  # vertices on the longest downward path
        long_child = [-1] * n
        for v in reversed(order):  # @line L2
            p = parent[v]
            if p != -1:
                c = long_child[p]
                if c == -1 or height[v] > height[c] or (height[v] == height[c] and v < c):
                    long_child[p] = v
                    height[p] = height[v] + 1
        ladder_of = [0] * n
        top = [0] * n
        ladders: list[list[int]] = []
        for v in order:  # @line L3
            if v != root and long_child[parent[v]] == v:
                continue
            path = []  # @line L4
            x = v
            while x != -1:
                path.append(x)
                x = long_child[x]
            k = len(path)
            above = []  # @line L5
            x = parent[v]
            while x != -1 and len(above) < k:
                above.append(x)
                x = parent[x]
            above.reverse()
            ladder_id = len(ladders)
            ladders.append(above + path)
            for x in path:  # @line L6
                ladder_of[x] = ladder_id
                top[x] = depth[v] - len(above)
        up = [[p if p != -1 else root for p in parent]]
        levels = max(1, (n - 1).bit_length())
        for _ in range(1, levels):  # @line L7
            prev = up[-1]
            up.append([prev[prev[v]] for v in range(n)])
        self.n = n
        self.root = root
        self.parent = parent
        self.depth = depth
        self.height = height
        self.long_child = long_child
        self.ladders = ladders
        self.ladder_of = ladder_of
        self.top = top
        self._up = up

    def level_ancestor(self, v: int, d: int) -> int | None:
        """Return the ancestor of v at depth d, or None if 0 <= d <= depth(v) fails."""
        if not 0 <= v < self.n:
            raise IndexError("vertex out of range")
        if d < 0 or d > self.depth[v]:  # @line L8
            return None
        k = self.depth[v] - d  # @line L9
        if k == 0:
            return v
        i = k.bit_length() - 1  # @line L10
        u = self._up[i][v]
        return self.ladders[self.ladder_of[u]][d - self.top[u]]  # @line L11

    def kth_ancestor(self, v: int, k: int) -> int | None:
        """Return the ancestor of v at distance k, or None if it does not exist."""
        if not 0 <= v < self.n:
            raise IndexError("vertex out of range")
        return self.level_ancestor(v, self.depth[v] - k)
test_ladder_decomposition.py 102 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/ladder-decomposition/. The authors are listed in prov/VJP1A9.jsonl.

import random

import pytest

from ladder_decomposition import LadderLevelAncestor

PARENT = [-1, 0, 1, 2, 3, 4, 2, 6, 0, 8, 3, 7]


def tree_edges(parent: list[int]) -> list[tuple[int, int]]:
    return [(v, p) for v, p in enumerate(parent) if p >= 0]


def naive_la(parent: list[int], v: int, d: int) -> int | None:
    path = []
    while v != -1:
        path.append(v)
        v = parent[v]
    path.reverse()
    return path[d] if 0 <= d < len(path) else None


def test_page_example() -> None:
    t = LadderLevelAncestor(12, tree_edges(PARENT), 0)
    assert t.height == [6, 5, 4, 3, 2, 1, 3, 2, 2, 1, 1, 1]
    assert t.long_child == [1, 2, 3, 4, 5, -1, 7, 11, 9, -1, -1, -1]
    assert sorted(t.ladders) == sorted([[0, 1, 2, 3, 4, 5], [0, 8, 9], [0, 1, 2, 6, 7, 11], [3, 10]])
    assert t.level_ancestor(11, 2) == 2
    assert t.level_ancestor(10, 1) == 1
    assert t.level_ancestor(10, 3) == 3
    assert t.level_ancestor(10, 4) == 10
    assert t.level_ancestor(9, 0) == 0
    assert t.level_ancestor(9, 3) is None
    assert t.level_ancestor(5, -1) is None
    assert t.kth_ancestor(11, 4) == 1
    assert t.kth_ancestor(11, 6) is None


def test_single_vertex() -> None:
    t = LadderLevelAncestor(1, [], 0)
    assert t.ladders == [[0]]
    assert t.level_ancestor(0, 0) == 0
    assert t.level_ancestor(0, 1) is None


def test_star() -> None:
    n = 9
    t = LadderLevelAncestor(n, [(0, i) for i in range(1, n)], 0)
    assert len(t.ladders) == n - 1
    for v in range(1, n):
        assert t.level_ancestor(v, 0) == 0
        assert t.level_ancestor(v, 1) == v
    t = LadderLevelAncestor(n, [(0, i) for i in range(1, n)], 4)
    assert t.level_ancestor(7, 0) == 4
    assert t.level_ancestor(7, 1) == 0


def test_long_path() -> None:
    n = 100_000
    t = LadderLevelAncestor(n, [(i, i + 1) for i in range(n - 1)], 0)
    assert len(t.ladders) == 1
    rng = random.Random(3)
    for _ in range(5000):
        v = rng.randrange(n)
        d = rng.randrange(v + 1)
        assert t.level_ancestor(v, d) == d
    t = LadderLevelAncestor(n, [(i, i + 1) for i in range(n - 1)], n - 1)
    assert t.level_ancestor(0, 0) == n - 1
    assert t.level_ancestor(0, 10) == n - 11


def test_bad_input() -> None:
    with pytest.raises(ValueError):
        LadderLevelAncestor(0, [], 0)
    with pytest.raises(ValueError):
        LadderLevelAncestor(3, [(0, 1)], 0)
    with pytest.raises(ValueError):
        LadderLevelAncestor(4, [(0, 1), (1, 0), (2, 3)], 0)
    t = LadderLevelAncestor(2, [(0, 1)], 0)
    with pytest.raises(IndexError):
        t.level_ancestor(2, 0)


def test_random_against_naive() -> None:
    rng = random.Random(17)
    for _ in range(600):
        n = rng.randint(1, 60)
        perm = list(range(n))
        rng.shuffle(perm)
        parent = [-1] * n
        for i in range(1, n):
            parent[perm[i]] = perm[rng.randrange(max(0, i - 3) if rng.random() < 0.5 else 0, i)]
        root = perm[0]
        t = LadderLevelAncestor(n, tree_edges(parent), root)
        assert sum(len(x) for x in t.ladders) <= 2 * n
        for _ in range(40):
            v = rng.randrange(n)
            d = rng.randint(-1, n)
            assert t.level_ancestor(v, d) == naive_la(parent, v, d)

What the next level needs

  • the tests did not run in this build

Read the full page about Ladder decomposition