Parent-pointer tree in Python
Rooted tree stored as an array in which each vertex records its parent.
- File
parent_pointer_tree.py, 125 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/parent-pointer-tree/. The authors are listed in prov/87C07Z.jsonl.
from typing import Iterator, Optional
NIL = -1
class ParentPointerTree:
"""A rooted forest on the vertices 0, 1, ..., n - 1 stored as one array.
parent[x] is the parent of x, or NIL (-1) if x is a root. A new tree
has the single vertex 0 as its root.
"""
def __init__(self, parent: Optional[list[int]] = None) -> None:
if parent is None:
parent = [NIL]
if not is_forest(parent):
raise ValueError("the parent array has a cycle or a bad index")
self.parent: list[int] = list(parent)
def size(self) -> int:
return len(self.parent)
def add_child(self, p: int) -> int:
"""Add a new vertex as a child of p and return it."""
if not 0 <= p < len(self.parent):
raise IndexError(p)
x = len(self.parent) # @line L1
self.parent.append(p) # @line L2
return x # @line L3
def parent_of(self, x: int) -> int:
return self.parent[x]
def path_to_root(self, x: int) -> Iterator[int]:
"""Yield x, the parent of x, and so on up to the root."""
while x != NIL: # @line L4
yield x # @line L5
x = self.parent[x] # @line L6
def root_of(self, x: int) -> int:
r = x
for r in self.path_to_root(x):
pass
return r
def depth(self, x: int) -> int:
return sum(1 for _ in self.path_to_root(x)) - 1
def child_lists(self) -> tuple[list[int], list[int]]:
"""Return (off, kids): the children of v are kids[off[v]:off[v + 1]],
in increasing order. Counting sort of the vertices by parent."""
n = len(self.parent)
off = [0] * (n + 1) # @line L7
for x in range(n): # @line L8
if self.parent[x] != NIL:
off[self.parent[x] + 1] += 1
for v in range(1, n + 1): # @line L9
off[v] += off[v - 1]
nxt = off[:n] # @line L10
kids = [0] * off[n]
for x in range(n): # @line L11
p = self.parent[x]
if p != NIL: # @line L12
kids[nxt[p]] = x # @line L13
nxt[p] += 1 # @line L14
return off, kids # @line L15
def children(self, v: int) -> list[int]:
"""The children of v by a scan of the whole array, in Theta(n) time."""
return [x for x in range(len(self.parent)) if self.parent[x] == v]
def lca(self, u: int, v: int) -> int:
"""Lowest common ancestor by lifting the deeper vertex, O(1 + depth).
Returns NIL if u and v are in different trees."""
du, dv = self.depth(u), self.depth(v)
while du > dv:
u, du = self.parent[u], du - 1
while dv > du:
v, dv = self.parent[v], dv - 1
while u != v:
u, v = self.parent[u], self.parent[v]
return u
def is_forest(parent: list[int]) -> bool:
"""True if every chain of parent links ends at NIL. Theta(n) time:
each vertex is coloured once (0 new, 1 on the current chain, 2 done)."""
n = len(parent)
for p in parent:
if p != NIL and not 0 <= p < n:
return False
colour = [0] * n
for s in range(n):
chain = []
x = s
while x != NIL and colour[x] == 0:
colour[x] = 1
chain.append(x)
x = parent[x]
if x != NIL and colour[x] == 1:
return False
for y in chain:
colour[y] = 2
return True
def from_bfs(adj: list[list[int]], root: int) -> list[int]:
"""The parent array of a breadth-first search tree of an undirected graph."""
parent = [NIL] * len(adj)
seen = [False] * len(adj)
seen[root] = True
queue = [root]
i = 0
while i < len(queue):
u = queue[i]
i += 1
for v in adj[u]:
if not seen[v]:
seen[v] = True
parent[v] = u
queue.append(v)
return parent
test_parent_pointer_tree.py 82 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/parent-pointer-tree/. The authors are listed in prov/87C07Z.jsonl.
import random
import pytest
from parent_pointer_tree import NIL, ParentPointerTree, from_bfs, is_forest
PARENT = [NIL, 3, 0, 0, 3, 2, 4]
def test_example() -> None:
t = ParentPointerTree(PARENT)
assert list(t.path_to_root(6)) == [6, 4, 3, 0]
assert t.depth(6) == 3
assert t.depth(0) == 0
assert t.root_of(5) == 0
off, kids = t.child_lists()
assert off == [0, 2, 2, 3, 5, 6, 6, 6]
assert kids == [2, 3, 5, 1, 4, 6]
assert t.children(3) == [1, 4]
assert t.lca(1, 6) == 3
assert t.lca(5, 6) == 0
assert t.lca(4, 6) == 4
def test_add_child() -> None:
t = ParentPointerTree()
a = t.add_child(0)
b = t.add_child(0)
c = t.add_child(a)
assert (a, b, c) == (1, 2, 3)
assert t.parent == [NIL, 0, 0, 1]
assert t.children(0) == [1, 2]
with pytest.raises(IndexError):
t.add_child(9)
def test_is_forest() -> None:
assert is_forest([NIL])
assert is_forest([NIL, NIL, 0])
assert not is_forest([1, 0])
assert not is_forest([0])
assert not is_forest([NIL, 5])
assert not is_forest([NIL, 2, 3, 1])
with pytest.raises(ValueError):
ParentPointerTree([1, 2, 0])
def test_forest_lca() -> None:
t = ParentPointerTree([NIL, 0, NIL, 2])
assert t.lca(1, 3) == NIL
assert t.root_of(3) == 2
def test_bfs_tree() -> None:
adj = [[1, 2], [0, 2, 3], [0, 1], [1]]
assert from_bfs(adj, 0) == [NIL, 0, 0, 1]
def test_random() -> None:
rng = random.Random(7)
for _ in range(200):
n = rng.randint(1, 40)
t = ParentPointerTree()
for _ in range(n - 1):
t.add_child(rng.randrange(t.size()))
off, kids = t.child_lists()
assert off[-1] == n - 1
for v in range(n):
assert kids[off[v]:off[v + 1]] == t.children(v)
for x in range(n):
path = list(t.path_to_root(x))
assert path[-1] == 0 and len(path) == t.depth(x) + 1
for a, b in zip(path, path[1:]):
assert t.parent[a] == b
u, v = rng.randrange(n), rng.randrange(n)
w = t.lca(u, v)
assert w in set(t.path_to_root(u)) & set(t.path_to_root(v))
assert all(c not in set(t.path_to_root(v)) for c in t.children(w)
if c in set(t.path_to_root(u)))
What the next level needs
- the tests did not run in this build