Algorithm Search⌘K

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
parent_pointer_tree.pyRawGitHub
# 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

Read the full page about Parent-pointer tree