Algorithm Search⌘K

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