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