Algorithm Search⌘K

Fully indexable dictionary in Python

A compressed set representation supporting rank, select and access, introduced by Raman, Raman and Rao.

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

from collections.abc import Iterable

W = 64
S = 8  # words in a superblock


class FullyIndexableDictionary:
    """A static set S of integers in [0, u) with member, rank, select, pred, succ
    and rank and select on the complement, all through rank and select on the
    characteristic bit vector of S.

    The primitives here use a plain packed vector with superblock counts:
    access and rank O(1), select O(log u) by binary search over the superblocks
    and a scan of at most 8 words. An RRR encoding makes all of them O(1).
    """

    def __init__(self, u: int, elements: Iterable[int] = ()) -> None:
        if u < 0:
            raise ValueError(u)
        self.u = u
        self.words = [0] * (u // W + 1)
        for x in elements:
            if not 0 <= x < u:
                raise ValueError(x)
            self.words[x // W] |= 1 << (x % W)
        self.RS: list[int] = []
        c = 0
        for k, x in enumerate(self.words):
            if k % S == 0:
                self.RS.append(c)
            c += x.bit_count()
        self.n = c

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

    # primitives on the characteristic vector B

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

    def _rank1(self, r: int) -> int:
        k = r // W
        c = self.RS[k // S]
        for t in range((k // S) * S, k):
            c += self.words[t].bit_count()
        return c + (self.words[k] & ((1 << (r % W)) - 1)).bit_count()

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

    def _select(self, j: int, bit: int) -> int:
        def before(q: int) -> int:  # number of `bit`s before superblock q
            return self.RS[q] if bit == 1 else q * S * W - self.RS[q]

        lo, hi = 0, len(self.RS) - 1
        while lo < hi:  # last superblock q with before(q) < j
            mid = (lo + hi + 1) // 2
            if before(mid) < j:
                lo = mid
            else:
                hi = mid - 1
        j -= before(lo)
        k = lo * S
        x = self._word(k, bit)
        while x.bit_count() < j:
            j -= x.bit_count()
            k += 1
            x = self._word(k, bit)
        for _ in range(j - 1):
            x &= x - 1
        return k * W + (x & -x).bit_length() - 1

    # the queries of the abstract type

    def member(self, x: int) -> bool:
        """Return True when x is in S, for 0 <= x < u."""
        if not 0 <= x < self.u:
            raise IndexError(x)
        return self._access(x) == 1  # @line L1

    def rank(self, r: int) -> int:
        """Return the number of elements less than r, for 0 <= r <= u."""
        if not 0 <= r <= self.u:
            raise IndexError(r)
        return self._rank1(r)  # @line L2

    def select(self, i: int) -> int:
        """Return the i-th smallest element, for 1 <= i <= n."""
        if not 1 <= i <= self.n:
            raise IndexError(i)
        return self._select(i, 1)  # @line L3

    def pred(self, x: int) -> int | None:
        """Return the largest element <= x, or None, for 0 <= x < u."""
        if not 0 <= x < self.u:
            raise IndexError(x)
        r = self._rank1(x + 1)  # @line L4
        if r == 0:  # @line L5
            return None
        return self._select(r, 1)  # @line L6

    def succ(self, x: int) -> int | None:
        """Return the smallest element >= x, or None, for 0 <= x < u."""
        if not 0 <= x < self.u:
            raise IndexError(x)
        r = self._rank1(x)  # @line L7
        if r == self.n:  # @line L8
            return None
        return self._select(r + 1, 1)  # @line L9

    def rank_out(self, r: int) -> int:
        """Return the number of non-elements less than r, for 0 <= r <= u."""
        if not 0 <= r <= self.u:
            raise IndexError(r)
        return r - self._rank1(r)  # @line L10

    def select_out(self, k: int) -> int:
        """Return the k-th smallest non-element, for 1 <= k <= u - n."""
        if not 1 <= k <= self.u - self.n:
            raise IndexError(k)
        return self._select(k, 0)  # @line L11
test_fully_indexable_dictionary.py 67 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/fully-indexable-dictionary/. The authors are listed in prov/ZEDY5H.jsonl.

import bisect
import random

import pytest

from fully_indexable_dictionary import FullyIndexableDictionary


def check_all(u: int, s: set[int]) -> None:
    f = FullyIndexableDictionary(u, s)
    ins = sorted(s)
    outs = [x for x in range(u) if x not in s]
    assert len(f) == len(ins)
    for x in range(u):
        assert f.member(x) == (x in s)
        k = bisect.bisect_right(ins, x)
        assert f.pred(x) == (ins[k - 1] if k else None)
        k = bisect.bisect_left(ins, x)
        assert f.succ(x) == (ins[k] if k < len(ins) else None)
    for r in range(u + 1):
        assert f.rank(r) == bisect.bisect_left(ins, r)
        assert f.rank_out(r) == bisect.bisect_left(outs, r)
    for i, x in enumerate(ins, 1):
        assert f.select(i) == x
    for i, x in enumerate(outs, 1):
        assert f.select_out(i) == x


def test_page_example() -> None:
    s = {0, 2, 3, 6, 8, 9, 13, 14, 15}
    f = FullyIndexableDictionary(16, s)
    assert not f.member(7)
    assert f.rank(10) == 6
    assert f.select(4) == 6
    assert f.pred(12) == 9
    assert f.succ(10) == 13
    assert f.succ(15) == 15
    assert f.pred(1) == 0
    assert f.rank_out(10) == 4
    assert f.select_out(5) == 10
    check_all(16, s)


def test_edges() -> None:
    check_all(0, set())
    check_all(1, set())
    check_all(1, {0})
    check_all(64, set(range(64)))
    check_all(700, {699})
    check_all(1100, {0, 1099})
    f = FullyIndexableDictionary(10, {5})
    assert f.pred(4) is None and f.succ(6) is None
    with pytest.raises(IndexError):
        f.select(2)
    with pytest.raises(ValueError):
        FullyIndexableDictionary(10, {10})


def test_random_against_sorted_lists() -> None:
    rng = random.Random(2002)
    for _ in range(500):
        u = rng.randrange(0, 1200 if rng.random() < 0.1 else 200)
        p = rng.random()
        check_all(u, {x for x in range(u) if rng.random() < p})

What the next level needs

  • the tests did not run in this build

Read the full page about Fully indexable dictionary