Balanced parentheses representation in Python
A succinct tree encoding by a balanced parenthesis sequence, supporting navigation through matching queries.
- File
balanced_parentheses_representation.py, 127 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/balanced-parentheses-representation/. The authors are listed in prov/8WYTP6.jsonl.
"""Balanced parentheses (BP) representation of an ordinal tree.
A node is named by the position of its opening parenthesis in P.
The primitives findclose, enclose and rmq are written as plain scans
over the excess, so each takes time linear in the distance; the range
min-max tree answers the same calls in O(log n), or O(1) with the
Navarro-Sadakane structure. Excess comes from a prefix-count array.
"""
from __future__ import annotations
from typing import Optional, Sequence
class BPTree:
"""A static ordinal tree stored as a balanced parenthesis sequence."""
def __init__(self, children: Sequence[Sequence[int]], root: int = 0) -> None:
"""Build P by a depth-first traversal; ``self.order`` is the preorder."""
p: list[int] = []
self.order: list[int] = []
stack: list[tuple[int, int]] = [(root, 0)]
p.append(1)
self.order.append(root)
while stack:
v, i = stack.pop()
if i < len(children[v]):
stack.append((v, i + 1))
c = children[v][i]
p.append(1)
self.order.append(c)
stack.append((c, 0))
else:
p.append(0)
self.P = p
self.n = len(self.order)
self.ones = [0]
for b in p:
self.ones.append(self.ones[-1] + b)
def text(self) -> str:
"""P as a string of parentheses."""
return "".join("(" if b else ")" for b in self.P)
def excess(self, i: int) -> int:
"""E(i): opens minus closes in P[0..i]; E(-1) = 0."""
return 2 * self.ones[i + 1] - (i + 1)
def findclose(self, i: int) -> int:
"""Position of the ')' matching the '(' at i."""
target = self.excess(i) - 1
j = i + 1
while self.excess(j) != target:
j += 1
return j
def enclose(self, i: int) -> Optional[int]:
"""Opening position of the tightest pair strictly containing i."""
target = self.excess(i) - 2
j = i - 1
while j >= 0 and self.excess(j) != target:
j -= 1
return j + 1 if target >= 0 else None
def rmq(self, i: int, j: int) -> int:
"""Leftmost position of the minimum excess in P[i..j]."""
best = i
for k in range(i + 1, j + 1):
if self.excess(k) < self.excess(best):
best = k
return best
def first_child(self, v: int) -> Optional[int]:
"""The first child of node v, or None for a leaf."""
if self.P[v + 1] == 1: # @line L1
return v + 1 # @line L2
return None
def next_sibling(self, v: int) -> Optional[int]:
"""The next sibling of node v, or None."""
j = self.findclose(v) # @line L3
if j + 1 < len(self.P) and self.P[j + 1] == 1: # @line L4
return j + 1 # @line L5
return None
def parent(self, v: int) -> Optional[int]:
"""The parent of node v, or None for the root."""
if self.excess(v) == 1: # @line L6
return None
return self.enclose(v) # @line L7
def subtree_size(self, v: int) -> int:
"""Number of nodes in the subtree of v, v included."""
return (self.findclose(v) - v + 1) // 2 # @line L8
def depth(self, v: int) -> int:
"""Depth of v, with the root at depth 0."""
return self.excess(v) - 1 # @line L9
def is_ancestor(self, u: int, v: int) -> bool:
"""True if u is an ancestor of v or u = v."""
return u <= v <= self.findclose(u) # @line L10
def lca(self, u: int, v: int) -> int:
"""Lowest common ancestor of nodes u and v."""
if u > v: # @line L11
u, v = v, u
if self.is_ancestor(u, v): # @line L12
return u
m = self.rmq(u, v) # @line L13
p = self.enclose(m + 1) # @line L14
assert p is not None
return p
def preorder_rank(self, v: int) -> int:
"""Preorder number of node v, from 0."""
return self.ones[v]
def degree(self, v: int) -> int:
"""Number of children, by walking the siblings (O(degree) calls)."""
d, c = 0, self.first_child(v)
while c is not None:
d += 1
c = self.next_sibling(c)
return d
test_balanced_parentheses_representation.py 101 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/balanced-parentheses-representation/. The authors are listed in prov/8WYTP6.jsonl.
"""Tests for the BP tree."""
from __future__ import annotations
import random
import unittest
from balanced_parentheses_representation import BPTree
# 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 TestBP(unittest.TestCase):
def test_example(self) -> None:
t = BPTree(EXAMPLE)
self.assertEqual(t.text(), "((()())()((())))")
pos = {t.order[r]: i for r, i in enumerate(i for i, b in enumerate(t.P) if b)}
A, B, C, D, E, F, G, H = (pos[x] for x in range(8))
self.assertEqual([A, B, E, F, C, D, G, H], [0, 1, 2, 4, 7, 9, 10, 11])
self.assertEqual(t.first_child(B), E)
self.assertIsNone(t.first_child(E))
self.assertEqual(t.next_sibling(B), C)
self.assertIsNone(t.next_sibling(D))
self.assertEqual(t.parent(F), B)
self.assertEqual(t.parent(H), G)
self.assertIsNone(t.parent(A))
self.assertEqual(t.subtree_size(D), 3)
self.assertEqual(t.subtree_size(A), 8)
self.assertEqual(t.depth(H), 3)
self.assertTrue(t.is_ancestor(D, H))
self.assertFalse(t.is_ancestor(B, H))
self.assertEqual(t.lca(E, F), B)
self.assertEqual(t.lca(F, H), A)
self.assertEqual(t.lca(G, H), G)
self.assertEqual(t.preorder_rank(C), 4)
self.assertEqual(t.degree(A), 3)
def test_single(self) -> None:
t = BPTree([[]])
self.assertEqual(t.text(), "()")
self.assertIsNone(t.first_child(0))
self.assertIsNone(t.next_sibling(0))
self.assertIsNone(t.parent(0))
self.assertEqual(t.subtree_size(0), 1)
def test_random(self) -> None:
rng = random.Random(8)
for _ in range(500):
n = rng.randint(1, 30)
ch = random_tree(rng, n)
t = BPTree(ch)
opens = [i for i, b in enumerate(t.P) if b]
pos = {t.order[r]: i for r, i in enumerate(opens)}
par = {c: v for v in range(n) for c in ch[v]}
depth = {0: 0}
size = [1] * n
for v in t.order:
for c in ch[v]:
depth[c] = depth[v] + 1
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.first_child(x), pos[ch[v][0]] if ch[v] else None)
self.assertEqual(t.parent(x), pos[par[v]] if v in par else None)
self.assertEqual(t.subtree_size(x), size[v])
self.assertEqual(t.depth(x), depth[v])
self.assertEqual(t.degree(x), len(ch[v]))
if v in par:
sib = ch[par[v]]
k = sib.index(v)
self.assertEqual(t.next_sibling(x), pos[sib[k + 1]] if k + 1 < len(sib) else None)
anc = {v: {v} for v in range(n)}
for v in t.order:
if v in par:
anc[v] = anc[par[v]] | {v}
for _ in range(10):
a, b = rng.randrange(n), rng.randrange(n)
common = anc[a] & anc[b]
want = max(common, key=lambda z: depth[z])
self.assertEqual(t.lca(pos[a], pos[b]), pos[want])
self.assertEqual(t.is_ancestor(pos[a], pos[b]), a in anc[b])
if __name__ == "__main__":
unittest.main()
What the next level needs
- the tests did not run in this build
Read the full page about Balanced parentheses representation