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
# 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