Algorithm Search⌘K

Wavelet tree in Python

A balanced binary tree over the alphabet with a bit vector per node, giving access, rank and select on sequences in compressed space.

File
wavelet_tree.py, 149 lines
Level
I0 not checked yet
Toolchain
Python 3.14, pytest 9
Test framework
pytest
Run the tests
python3 -m pytest
wavelet_tree.pyRawGitHub
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/wavelet-tree/. The authors are listed in prov/K5P9KD.jsonl.

from __future__ import annotations

from collections.abc import Sequence

W = 64


class RankBits:
    """A static bit vector with rank in O(1) (a count before each word) and
    select by binary search over those counts."""

    def __init__(self, bits: Sequence[int]) -> None:
        self.n = len(bits)
        self.words = [0] * (self.n // W + 1)
        for i, x in enumerate(bits):
            if x:
                self.words[i // W] |= 1 << (i % W)
        self.before = [0]
        for x in self.words:
            self.before.append(self.before[-1] + x.bit_count())

    def __len__(self) -> int:
        return self.n

    def access(self, i: int) -> int:
        return (self.words[i // W] >> (i % W)) & 1

    def rank1(self, i: int) -> int:
        k = i // W
        return self.before[k] + (self.words[k] & ((1 << (i % W)) - 1)).bit_count()

    def rank0(self, i: int) -> int:
        return i - self.rank1(i)

    def select(self, j: int, bit: int) -> int:
        """Return the position of the j-th bit equal to `bit`, for j >= 1."""
        def count(k: int) -> int:  # wanted bits in words 0..k-1
            return self.before[k] if bit else k * W - self.before[k]

        lo, hi = 0, len(self.words) - 1
        while lo < hi:
            mid = (lo + hi + 1) // 2
            if count(mid) < j:
                lo = mid
            else:
                hi = mid - 1
        x = self.words[lo] if bit else ~self.words[lo]
        for _ in range(j - count(lo) - 1):
            x &= x - 1
        p = lo * W + (x & -x).bit_length() - 1
        if p >= self.n:
            raise IndexError(j)
        return p


class _Node:
    __slots__ = ("bits", "left", "right")

    def __init__(self) -> None:
        self.bits = RankBits([])
        self.left: _Node | None = None
        self.right: _Node | None = None


class WaveletTree:
    """A wavelet tree over the alphabet [0, sigma) with a bit vector at each inner node."""

    def __init__(self, seq: Sequence[int], sigma: int) -> None:
        if sigma < 1 or any(not 0 <= c < sigma for c in seq):
            raise ValueError("symbols must be in [0, sigma)")
        self.n, self.sigma = len(seq), sigma
        self.root = self._build(list(seq), 0, sigma)

    def _build(self, s: list[int], a: int, b: int) -> _Node:
        v = _Node()
        if b - a > 1:
            m = (a + b) // 2
            v.bits = RankBits([1 if c >= m else 0 for c in s])
            v.left = self._build([c for c in s if c < m], a, m)
            v.right = self._build([c for c in s if c >= m], m, b)
        return v

    def __len__(self) -> int:
        return self.n

    def access(self, i: int) -> int:
        """Return S[i], for 0 <= i < n."""
        if not 0 <= i < self.n:
            raise IndexError(i)
        v, a, b = self.root, 0, self.sigma  # @line L1
        while b - a > 1:  # @line L2
            m = (a + b) // 2  # @line L3
            if v.bits.access(i) == 0:  # @line L4
                i, v, b = v.bits.rank0(i), v.left, m
            else:  # @line L5
                i, v, a = v.bits.rank1(i), v.right, m
            assert v is not None
        return a  # @line L6

    def rank(self, c: int, i: int) -> int:
        """Return the number of c in S[0..i-1], for 0 <= i <= n."""
        if not 0 <= i <= self.n:
            raise IndexError(i)
        if not 0 <= c < self.sigma:
            return 0
        v, a, b = self.root, 0, self.sigma  # @line L7
        while b - a > 1:  # @line L8
            m = (a + b) // 2  # @line L9
            if c < m:  # @line L10
                i, v, b = v.bits.rank0(i), v.left, m
            else:  # @line L11
                i, v, a = v.bits.rank1(i), v.right, m
            assert v is not None
        return i  # @line L12

    def select(self, c: int, j: int) -> int:
        """Return the position of the j-th c, for 1 <= j <= rank(c, n)."""
        if not 0 <= c < self.sigma or not 1 <= j <= self.rank(c, self.n):
            raise IndexError((c, j))
        return self._select(self.root, 0, self.sigma, c, j)

    def _select(self, v: _Node, a: int, b: int, c: int, j: int) -> int:
        if b - a == 1:  # @line L13
            return j - 1
        m = (a + b) // 2  # @line L14
        if c < m:  # @line L15
            assert v.left is not None
            return v.bits.select(self._select(v.left, a, m, c, j) + 1, 0)
        assert v.right is not None  # @line L16
        return v.bits.select(self._select(v.right, m, b, c, j) + 1, 1)

    def quantile(self, l: int, r: int, k: int) -> int:
        """Return the k-th smallest symbol of S[l..r-1], for 1 <= k <= r - l."""
        if not 0 <= l < r <= self.n or not 1 <= k <= r - l:
            raise IndexError((l, r, k))
        v, a, b = self.root, 0, self.sigma  # @line L17
        while b - a > 1:  # @line L18
            m = (a + b) // 2  # @line L19
            z = v.bits.rank0(r) - v.bits.rank0(l)
            if k <= z:  # @line L20
                l, r, v, b = v.bits.rank0(l), v.bits.rank0(r), v.left, m
            else:  # @line L21
                k -= z
                l, r, v, a = v.bits.rank1(l), v.bits.rank1(r), v.right, m
            assert v is not None
        return a  # @line L22
test_wavelet_tree.py 74 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/wavelet-tree/. The authors are listed in prov/K5P9KD.jsonl.

import random

import pytest

from wavelet_tree import WaveletTree

CODE = {"a": 0, "b": 1, "c": 2, "d": 3, "r": 4}
S = [CODE[ch] for ch in "abracadabra"]


def bits(v) -> str:
    return "".join(str(v.bits.access(i)) for i in range(len(v.bits)))


def check_all(seq: list[int], sigma: int, rng: random.Random) -> None:
    t = WaveletTree(seq, sigma)
    n = len(seq)
    assert [t.access(i) for i in range(n)] == seq
    for c in range(sigma):
        r = 0
        for i in range(n + 1):
            assert t.rank(c, i) == r
            if i < n and seq[i] == c:
                r += 1
        pos = [i for i, x in enumerate(seq) if x == c]
        for j, p in enumerate(pos, 1):
            assert t.select(c, j) == p
    for _ in range(30 if n else 0):
        l = rng.randrange(n)
        r = rng.randrange(l + 1, n + 1)
        k = rng.randrange(1, r - l + 1)
        assert t.quantile(l, r, k) == sorted(seq[l:r])[k - 1]


def test_page_example() -> None:
    t = WaveletTree(S, 5)
    assert bits(t.root) == "00101010010"
    assert bits(t.root.left) == "0100010"
    assert bits(t.root.right) == "1011"
    assert bits(t.root.right.right) == "101"
    assert t.access(4) == CODE["c"]
    assert t.rank(CODE["a"], 7) == 3
    assert t.select(CODE["a"], 4) == 7
    assert t.rank(CODE["b"], 9) == 2
    assert t.select(CODE["r"], 2) == 9
    assert t.quantile(2, 9, 4) == CODE["b"]
    check_all(S, 5, random.Random(1))


def test_edges() -> None:
    rng = random.Random(2)
    check_all([], 1, rng)
    check_all([0], 1, rng)
    check_all([0, 0, 0], 1, rng)
    check_all([3, 3, 3], 4, rng)
    check_all(list(range(100)), 100, rng)
    check_all([1, 0] * 70, 2, rng)
    t = WaveletTree([0, 2], 4)
    assert t.rank(1, 2) == 0 and t.rank(3, 2) == 0
    with pytest.raises(IndexError):
        t.select(1, 1)
    with pytest.raises(ValueError):
        WaveletTree([4], 4)


def test_random_against_lists() -> None:
    rng = random.Random(2003)
    for _ in range(500):
        sigma = rng.randrange(1, 20)
        n = rng.randrange(0, 80)
        check_all([rng.randrange(sigma) for _ in range(n)], sigma, rng)

What the next level needs

  • the tests did not run in this build

Read the full page about Wavelet tree