Algorithm Search⌘K

LOUDS tree in Python

A succinct tree encoding by the level-order unary degree sequence, using 2n + o(n) bits.

File
louds_tree.py, 107 lines
Level
I0 not checked yet
Toolchain
Python 3.14, pytest 9
Test framework
pytest
Run the tests
python3 -m pytest
louds_tree.pyRawGitHub
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/louds-tree/. The authors are listed in prov/QA72X1.jsonl.
"""LOUDS tree: an ordinal tree as a level-order unary degree sequence.

Nodes are named by their level-order number k, with the root at k = 0.
The bit string is L = 10 1^d0 0 1^d1 0 ... 1^d(n-1) 0, of 2n + 1 bits.
Rank and select are answered from plain lookup arrays, which keeps the
code short; a real succinct index (see rank-select-dictionary) answers
the same calls in O(1) time with o(n) extra bits.
"""

from __future__ import annotations

from collections import deque
from typing import Optional, Sequence


class _Bits:
    """A static bit string with rank and select by lookup arrays."""

    def __init__(self, bits: Sequence[int]) -> None:
        self.bits = list(bits)
        self.ones_before = [0]
        self.pos1: list[int] = []
        self.pos0: list[int] = []
        for i, b in enumerate(self.bits):
            self.ones_before.append(self.ones_before[-1] + b)
            (self.pos1 if b else self.pos0).append(i)

    def rank1(self, i: int) -> int:
        """Number of 1s in positions 0..i-1."""
        return self.ones_before[i]

    def rank0(self, i: int) -> int:
        """Number of 0s in positions 0..i-1."""
        return i - self.ones_before[i]

    def select1(self, j: int) -> int:
        """Position of the j-th 1, j >= 1."""
        return self.pos1[j - 1]

    def select0(self, j: int) -> int:
        """Position of the j-th 0, j >= 1."""
        return self.pos0[j - 1]


class LoudsTree:
    """A static ordinal tree in the LOUDS encoding."""

    def __init__(self, children: Sequence[Sequence[int]], root: int = 0) -> None:
        """Build from child lists of any node numbering; nodes become level order.

        ``self.order[k]`` is the input node that became level-order node k.
        """
        bits = [1, 0]
        self.order: list[int] = []
        queue = deque([root])
        while queue:
            v = queue.popleft()
            self.order.append(v)
            for c in children[v]:
                bits.append(1)
                queue.append(c)
            bits.append(0)
        self.n = len(self.order)
        self.L = _Bits(bits)

    def bitstring(self) -> str:
        """The LOUDS string as text."""
        return "".join(map(str, self.L.bits))

    def child(self, k: int, i: int) -> Optional[int]:
        """The i-th child (1-based) of node k, or None."""
        s = self.L.select0(k + 1)  # @line L1
        if i < 1 or i > self.degree(k):  # @line L2
            return None
        return self.L.rank1(s) + i - 1  # @line L3

    def degree(self, k: int) -> int:
        """Number of children of node k."""
        return self.L.select0(k + 2) - self.L.select0(k + 1) - 1  # @line L4

    def is_leaf(self, k: int) -> bool:
        """True if node k has no children."""
        return self.L.bits[self.L.select0(k + 1) + 1] == 0

    def parent(self, k: int) -> Optional[int]:
        """The parent of node k, or None for the root."""
        if k == 0:  # @line L5
            return None
        p = self.L.select1(k + 1)  # @line L6
        return self.L.rank0(p) - 1  # @line L7

    def next_sibling(self, k: int) -> Optional[int]:
        """The next sibling of node k, or None."""
        p = self.L.select1(k + 1)  # @line L8
        return k + 1 if self.L.bits[p + 1] == 1 else None  # @line L9

    def prev_sibling(self, k: int) -> Optional[int]:
        """The previous sibling of node k, or None."""
        p = self.L.select1(k + 1)
        return k - 1 if self.L.bits[p - 1] == 1 and k > 0 else None

    def child_rank(self, k: int) -> int:
        """The position (1-based) of node k among its siblings; k >= 1."""
        p = self.L.select1(k + 1)  # @line L10
        return p - self.L.select0(self.L.rank0(p))  # @line L11
test_louds_tree.py 101 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/louds-tree/. The authors are listed in prov/QA72X1.jsonl.
"""Tests for the LOUDS tree."""

from __future__ import annotations

import random
import unittest
from collections import deque

from louds_tree import LoudsTree

# A: B C D; B: E F; D: G; G: H  (input numbering A=0 .. H=7)
EXAMPLE = [[1, 2, 3], [4, 5], [], [6], [], [], [7], []]


def random_tree(rng: random.Random, n: int) -> list[list[int]]:
    """A random ordered tree on nodes 0..n-1 rooted at 0."""
    ch: list[list[int]] = [[] for _ in range(n)]
    for v in range(1, n):
        ch[rng.randrange(v)].append(v)
    for c in ch:
        rng.shuffle(c)
    return ch


def level_order(ch: list[list[int]]) -> list[int]:
    out, q = [], deque([0])
    while q:
        v = q.popleft()
        out.append(v)
        q.extend(ch[v])
    return out


class TestLouds(unittest.TestCase):
    def test_example(self) -> None:
        t = LoudsTree(EXAMPLE)
        self.assertEqual(t.bitstring(), "10111011001000100")
        self.assertEqual(t.degree(0), 3)
        self.assertEqual(t.child(0, 3), 3)
        self.assertEqual(t.child(1, 2), 5)
        self.assertEqual(t.parent(6), 3)
        self.assertEqual(t.next_sibling(1), 2)
        self.assertIsNone(t.next_sibling(3))
        self.assertEqual(t.child_rank(5), 2)
        self.assertIsNone(t.parent(0))
        self.assertIsNone(t.child(2, 1))
        self.assertTrue(t.is_leaf(7))

    def test_single_node(self) -> None:
        t = LoudsTree([[]])
        self.assertEqual(t.bitstring(), "100")
        self.assertEqual(t.degree(0), 0)
        self.assertIsNone(t.parent(0))
        self.assertIsNone(t.next_sibling(0))
        self.assertIsNone(t.prev_sibling(0))

    def test_path_and_star(self) -> None:
        path = [[1], [2], [3], []]
        t = LoudsTree(path)
        self.assertEqual(t.bitstring(), "101010100")
        self.assertEqual([t.parent(k) for k in range(1, 4)], [0, 1, 2])
        star = [[1, 2, 3, 4], [], [], [], []]
        s = LoudsTree(star)
        self.assertEqual(s.bitstring(), "10111100000")
        self.assertEqual([s.child_rank(k) for k in range(1, 5)], [1, 2, 3, 4])

    def test_random(self) -> None:
        rng = random.Random(72)
        for _ in range(500):
            n = rng.randint(1, 40)
            ch = random_tree(rng, n)
            t = LoudsTree(ch)
            order = level_order(ch)
            self.assertEqual(t.order, order)
            num = {v: k for k, v in enumerate(order)}
            self.assertEqual(len(t.bitstring()), 2 * n + 1)
            par = {c: v for v in range(n) for c in ch[v]}
            for v in range(n):
                k = num[v]
                self.assertEqual(t.degree(k), len(ch[v]))
                self.assertEqual(t.is_leaf(k), not ch[v])
                for i, c in enumerate(ch[v], 1):
                    self.assertEqual(t.child(k, i), num[c])
                self.assertIsNone(t.child(k, len(ch[v]) + 1))
                if v == 0:
                    self.assertIsNone(t.parent(k))
                    continue
                p = par[v]
                self.assertEqual(t.parent(k), num[p])
                idx = ch[p].index(v)
                self.assertEqual(t.child_rank(k), idx + 1)
                nxt = num[ch[p][idx + 1]] if idx + 1 < len(ch[p]) else None
                prv = num[ch[p][idx - 1]] if idx > 0 else None
                self.assertEqual(t.next_sibling(k), nxt)
                self.assertEqual(t.prev_sibling(k), prv)


if __name__ == "__main__":
    unittest.main()

What the next level needs

  • the tests did not run in this build

Read the full page about LOUDS tree