Algorithm Search⌘K

Rank/select dictionary in Python

A bit vector augmented with an o(n)-bit index that answers rank and select in constant time.

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

from collections.abc import Iterable

W = 64  # bits in a word
S = 8  # words in a superblock


class _SelectIndex:
    """Clark's select index over the increasing positions P of one kind of bit."""

    def __init__(self, P: list[int], k1: int, t1: int, k2: int, t2: int) -> None:
        self.k1, self.k2 = k1, k2
        self.G: list[int] = []
        self.sparse: list[bool] = []
        self.E1: list[list[int]] = []
        self.S: list[list[int]] = []
        self.E2: list[list[list[int] | None]] = []
        m = len(P)
        for g in range((m + k1 - 1) // k1):  # @line L1
            Q = P[g * k1 : min(m, (g + 1) * k1)]  # @line L2
            self.G.append(Q[0])  # @line L3
            subs: list[int] = []
            e2: list[list[int] | None] = []
            if Q[-1] - Q[0] >= t1:  # @line L4
                self.sparse.append(True)
                self.E1.append(list(Q))
            else:  # @line L5
                self.sparse.append(False)  # @line L6
                self.E1.append([])
                for h in range((len(Q) + k2 - 1) // k2):  # @line L7
                    R = Q[h * k2 : min(len(Q), (h + 1) * k2)]  # @line L8
                    subs.append(R[0] - Q[0])  # @line L9
                    if R[-1] - R[0] >= t2:  # @line L10
                        e2.append([r - Q[0] for r in R])
                    else:  # @line L11
                        e2.append(None)
            self.S.append(subs)
            self.E2.append(e2)


class RankSelectDictionary:
    """A static bit vector with O(1) rank and select (Jacobson rank, Clark select).

    Rank: superblocks of 8 words and one relative count per word, with popcount.
    Select: groups of k1 ones, stored in full if they span >= t1 bits, else cut
    into subgroups of k2 ones, stored in full if they span >= t2 bits, else scanned.
    The default parameters are the ones of the page; tests may pass smaller ones.
    Cost: build O(n); access, rank and select O(1) word operations for the default parameters.
    """

    def __init__(self, bits: Iterable[int] = (), params: tuple[int, int, int, int] | None = None) -> None:
        b = [1 if x else 0 for x in bits]
        n = len(b)
        self.n = n
        self.words = [0] * (n // W + 1)
        for i, x in enumerate(b):
            if x:
                self.words[i // W] |= 1 << (i % W)
        self.RS: list[int] = []
        self.RB: list[int] = []
        c = 0
        for k, x in enumerate(self.words):
            if k % S == 0:
                self.RS.append(c)
            self.RB.append(c - self.RS[k // S])
            c += x.bit_count()
        self.ones = c
        if params is None:
            ell = max(2, n.bit_length())  # ceil(lg(n + 1))
            lgl = max(1, (ell - 1).bit_length())
            params = (ell * ell, ell**4, lgl * lgl, lgl**4)
        self.params = params
        k1, t1, k2, t2 = params
        self.sel1 = _SelectIndex([i for i in range(n) if b[i]], k1, t1, k2, t2)
        self.sel0 = _SelectIndex([i for i in range(n) if not b[i]], k1, t1, k2, t2)

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

    def access(self, i: int) -> int:
        """Return B[i], for 0 <= i < n."""
        if not 0 <= i < self.n:
            raise IndexError(i)
        return (self.words[i // W] >> (i % W)) & 1

    def rank1(self, i: int) -> int:
        """Return the number of 1s in B[0 .. i-1], for 0 <= i <= n."""
        if not 0 <= i <= self.n:
            raise IndexError(i)
        k = i // W  # @line L17
        return self.RS[k // S] + self.RB[k] + (self.words[k] & ((1 << (i % W)) - 1)).bit_count()  # @line L18

    def rank0(self, i: int) -> int:
        """Return the number of 0s in B[0 .. i-1], for 0 <= i <= n."""
        return i - self.rank1(i)

    def _word(self, k: int, bit: int) -> int:
        x = self.words[k]
        if bit == 0:
            x = ~x & ((1 << W) - 1)
            last = self.n // W
            if k == last:
                x &= (1 << (self.n % W)) - 1
        return x

    def _scan(self, p: int, u: int, bit: int) -> int:
        """Return the position of the u-th bit equal to `bit` at or after position p."""
        k = p // W
        x = self._word(k, bit) & ~((1 << (p % W)) - 1)
        while x.bit_count() < u:
            u -= x.bit_count()
            k += 1
            x = self._word(k, bit)
        for _ in range(u - 1):
            x &= x - 1
        return k * W + (x & -x).bit_length() - 1

    def _select(self, ix: _SelectIndex, j: int, bit: int) -> int:
        g, t = (j - 1) // ix.k1, (j - 1) % ix.k1  # @line L12
        if ix.sparse[g]:  # @line L13
            return ix.E1[g][t]
        h, u = t // ix.k2, t % ix.k2  # @line L14
        e = ix.E2[g][h]
        if e is not None:  # @line L15
            return ix.G[g] + e[u]
        return self._scan(ix.G[g] + ix.S[g][h], u + 1, bit)  # @line L16

    def select1(self, j: int) -> int:
        """Return the position of the j-th 1, for 1 <= j <= rank1(n)."""
        if not 1 <= j <= self.ones:
            raise IndexError(j)
        return self._select(self.sel1, j, 1)

    def select0(self, j: int) -> int:
        """Return the position of the j-th 0, for 1 <= j <= rank0(n)."""
        if not 1 <= j <= self.n - self.ones:
            raise IndexError(j)
        return self._select(self.sel0, j, 0)
test_rank_select_dictionary.py 68 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/rank-select-dictionary/. The authors are listed in prov/P99JRP.jsonl.

import itertools
import random

import pytest

from rank_select_dictionary import RankSelectDictionary


def check_all(b: list[int], params: tuple[int, int, int, int] | None = None) -> None:
    d = RankSelectDictionary(b, params)
    pref = list(itertools.accumulate(b, initial=0))
    assert len(d) == len(b)
    for i in range(len(b)):
        assert d.access(i) == b[i]
    for i in range(len(b) + 1):
        assert d.rank1(i) == pref[i]
        assert d.rank0(i) == i - pref[i]
    for j, p in enumerate([i for i, x in enumerate(b) if x], 1):
        assert d.select1(j) == p
    for j, p in enumerate([i for i, x in enumerate(b) if not x], 1):
        assert d.select0(j) == p


def test_page_example() -> None:
    b = [int(c) for c in "10110010110001110000000001"]
    d = RankSelectDictionary(b, (4, 7, 2, 3))
    ix = d.sel1
    assert ix.G == [0, 8, 15]
    assert ix.sparse == [False, False, True]
    assert ix.S[0] == [0, 3] and ix.S[1] == [0, 5]
    assert ix.E2[0] == [None, [3, 6]]
    assert ix.E1[2] == [15, 25]
    assert [d.select1(j) for j in (4, 6, 8, 10)] == [6, 9, 14, 25]
    check_all(b, (4, 7, 2, 3))


def test_edges() -> None:
    check_all([])
    check_all([1])
    check_all([0])
    check_all([1] * 200)
    check_all([0] * 130 + [1])
    check_all([1] + [0] * 300 + [1], (2, 50, 1, 1))
    with pytest.raises(IndexError):
        RankSelectDictionary([0, 0]).select1(1)
    with pytest.raises(IndexError):
        RankSelectDictionary([1, 1]).select0(1)


def test_random_default_parameters() -> None:
    rng = random.Random(1996)
    for _ in range(300):
        n = rng.randrange(0, 700)
        p = rng.random() ** 2
        check_all([1 if rng.random() < p else 0 for _ in range(n)])


def test_random_small_parameters() -> None:
    rng = random.Random(7)
    for _ in range(400):
        n = rng.randrange(0, 300)
        p = rng.random()
        k2 = rng.randrange(1, 4)
        params = (k2 * rng.randrange(1, 4), rng.randrange(1, 60), k2, rng.randrange(1, 20))
        check_all([1 if rng.random() < p else 0 for _ in range(n)], params)

What the next level needs

  • the tests did not run in this build

Read the full page about Rank/select dictionary