DFUDS in Python
A succinct tree encoding by the depth-first unary degree sequence, which supports child-rank navigation.
- File
dfuds.py, 126 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/dfuds/. The authors are listed in prov/BT52AM.jsonl.
"""DFUDS: the depth-first unary degree sequence of an ordinal tree.
U = ( followed by, for each node in preorder, d opening parentheses and
one closing one, where d is its degree. U is balanced, of 2n symbols
counted with the leading '('. A node is named by the position where its
code starts; the root is at 1. findclose, findopen and fwdsearch are
plain scans here; the range min-max tree makes them O(log n) or O(1).
"""
from __future__ import annotations
from typing import Optional, Sequence
class DFUDS:
"""A static ordinal tree in the DFUDS encoding."""
def __init__(self, children: Sequence[Sequence[int]], root: int = 0) -> None:
"""Build U by preorder; ``self.order`` is the preorder of the input nodes."""
u = [1]
self.order: list[int] = []
stack = [root]
while stack:
v = stack.pop()
self.order.append(v)
u.extend([1] * len(children[v]))
u.append(0)
stack.extend(reversed(children[v]))
self.U = u
self.n = len(self.order)
self.ones = [0]
for b in u:
self.ones.append(self.ones[-1] + b)
self.zeros = [i for i, b in enumerate(u) if b == 0]
def text(self) -> str:
"""U as a string of parentheses."""
return "".join("(" if b else ")" for b in self.U)
# --- primitives -------------------------------------------------------
def excess(self, i: int) -> int:
"""Opens minus closes in U[0..i]; excess(-1) = 0."""
return 2 * self.ones[i + 1] - (i + 1)
def rank0(self, i: int) -> int:
"""Number of ')' in U[0..i-1]."""
return i - self.ones[i]
def select0(self, j: int) -> int:
"""Position of the j-th ')', j >= 1; select0(0) = 0 by convention."""
return self.zeros[j - 1] if j > 0 else 0
def fwdsearch(self, i: int, d: int) -> int:
"""Least j > i with excess(j) = excess(i) + d."""
target = self.excess(i) + d
j = i + 1
while self.excess(j) != target:
j += 1
return j
def findclose(self, i: int) -> int:
"""The ')' matching the '(' at i."""
return self.fwdsearch(i, -1)
def findopen(self, j: int) -> int:
"""The '(' matching the ')' at j."""
target = self.excess(j)
i = j - 1
while self.excess(i) != target:
i -= 1
return i + 1
# --- operations -------------------------------------------------------
def root(self) -> int:
"""The root node, at position 1."""
return 1
def degree(self, v: int) -> int:
"""Number of children of v."""
return self.select0(self.rank0(v) + 1) - v # @line L1
def child(self, v: int, i: int) -> Optional[int]:
"""The i-th child (1-based) of v, or None."""
if i < 1 or i > self.degree(v): # @line L2
return None
e = self.select0(self.rank0(v) + 1) # @line L3
return self.findclose(e - i) + 1 # @line L4
def parent(self, v: int) -> Optional[int]:
"""The parent of v, or None for the root."""
if v == 1: # @line L5
return None
o = self.findopen(v - 1) # @line L6
return self.select0(self.rank0(o)) + 1 # @line L7
def child_rank(self, v: int) -> int:
"""The index (1-based) of v among its siblings; v is not the root."""
o = self.findopen(v - 1) # @line L8
return self.select0(self.rank0(o) + 1) - o # @line L9
def subtree_size(self, v: int) -> int:
"""Number of nodes in the subtree of v."""
end = self.fwdsearch(v - 1, -1) # @line L10
return (end - v) // 2 + 1 # @line L11
def next_sibling(self, v: int) -> Optional[int]:
"""The next sibling of v, or None."""
if v == 1:
return None
p = self.parent(v)
assert p is not None
return self.child(p, self.child_rank(v) + 1)
def is_leaf(self, v: int) -> bool:
"""True if v has no children."""
return self.U[v] == 0
def preorder_rank(self, v: int) -> int:
"""Preorder number of v, from 0."""
return self.rank0(v)
def node_at(self, k: int) -> int:
"""The node with preorder number k."""
return 1 if k == 0 else self.select0(k) + 1
test_dfuds.py 85 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/dfuds/. The authors are listed in prov/BT52AM.jsonl.
"""Tests for DFUDS."""
from __future__ import annotations
import random
import unittest
from dfuds import DFUDS
# 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
class TestDFUDS(unittest.TestCase):
def test_example(self) -> None:
t = DFUDS(EXAMPLE)
self.assertEqual(t.text(), "(((()(())))()())")
pos = {v: t.node_at(k) for k, v in enumerate(t.order)}
self.assertEqual([pos[x] for x in range(8)], [1, 5, 10, 11, 8, 9, 13, 15])
A, B, C, D, E, F, G, H = (pos[x] for x in range(8))
self.assertEqual(t.degree(A), 3)
self.assertEqual([t.child(A, i) for i in (1, 2, 3)], [B, C, D])
self.assertIsNone(t.child(A, 4))
self.assertEqual(t.parent(F), B)
self.assertEqual(t.parent(D), A)
self.assertIsNone(t.parent(A))
self.assertEqual(t.child_rank(D), 3)
self.assertEqual(t.subtree_size(D), 3)
self.assertEqual(t.subtree_size(A), 8)
self.assertEqual(t.next_sibling(E), F)
self.assertIsNone(t.next_sibling(F))
self.assertTrue(t.is_leaf(H))
self.assertEqual(t.preorder_rank(C), 4)
def test_single(self) -> None:
t = DFUDS([[]])
self.assertEqual(t.text(), "()")
self.assertEqual(t.degree(1), 0)
self.assertEqual(t.subtree_size(1), 1)
self.assertIsNone(t.parent(1))
def test_random(self) -> None:
rng = random.Random(52)
for _ in range(500):
n = rng.randint(1, 30)
ch = random_tree(rng, n)
t = DFUDS(ch)
self.assertEqual(len(t.U), 2 * n)
pos = {v: t.node_at(k) for k, v in enumerate(t.order)}
par = {c: v for v in range(n) for c in ch[v]}
size = [1] * n
for v in reversed(t.order):
if v in par:
size[par[v]] += size[v]
for v in range(n):
x = pos[v]
self.assertEqual(t.preorder_rank(x), t.order.index(v))
self.assertEqual(t.degree(x), len(ch[v]))
self.assertEqual(t.is_leaf(x), not ch[v])
self.assertEqual([t.child(x, i) for i in range(1, len(ch[v]) + 1)], [pos[c] for c in ch[v]])
self.assertEqual(t.subtree_size(x), size[v])
if v in par:
self.assertEqual(t.parent(x), pos[par[v]])
sib = ch[par[v]]
k = sib.index(v)
self.assertEqual(t.child_rank(x), k + 1)
self.assertEqual(t.next_sibling(x), pos[sib[k + 1]] if k + 1 < len(sib) else None)
else:
self.assertIsNone(t.parent(x))
if __name__ == "__main__":
unittest.main()
What the next level needs
- the tests did not run in this build