Algorithm Search⌘K

Jacobson's rank structure in Python

Jacobson's 1989 n + o(n)-bit structure answering rank in O(1) time, the first succinct rank index.

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

from collections.abc import Iterable


class JacobsonRank:
    """Jacobson's two-level rank directory with a lookup table for short blocks.

    Blocks have b = max(1, floor(ceil(lg n) / 2)) bits and a superblock holds s = 2b blocks.
    RS holds the rank at each superblock start, RB the rank at each block start
    relative to its superblock, and T[x][r] the number of 1s in the r low bits of x.
    Cost: build O(n), rank O(1), select O(log n) by binary search.
    """

    def __init__(self, bits: Iterable[int] = ()) -> None:
        bv = [1 if x else 0 for x in bits]
        n = len(bv)
        self.n = n
        lg = max(1, (n - 1).bit_length()) if n > 1 else 1
        b = max(1, lg // 2)  # @line L1
        s = 2 * b
        big_k = n // b
        self.b, self.s = b, s
        self.T = [[(x & ((1 << r) - 1)).bit_count() for r in range(b + 1)] for x in range(1 << b)]  # @line L2
        self.RS: list[int] = []
        self.RB: list[int] = []
        self.X: list[int] = []
        c = 0  # @line L3
        for k in range(big_k + 1):  # @line L4
            if k % s == 0:  # @line L5
                self.RS.append(c)
            self.RB.append(c - self.RS[k // s])  # @line L6
            x = 0  # @line L7
            for t in range(b):
                p = k * b + t
                if p < n and bv[p]:
                    x |= 1 << t
            self.X.append(x)
            c += self.T[x][b]  # @line L8
        self.ones = c

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

    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 // self.b  # @line L9
        return self.RS[k // self.s] + self.RB[k] + self.T[self.X[k]][i % self.b]  # @line L10

    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 access(self, i: int) -> int:
        """Return B[i], for 0 <= i < n."""
        if not 0 <= i < self.n:
            raise IndexError(i)
        return (self.X[i // self.b] >> (i % self.b)) & 1

    def select1(self, j: int) -> int:
        """Return the position of the j-th 1, for 1 <= j <= rank1(n), in O(log n) rank calls."""
        if not 1 <= j <= self.ones:
            raise IndexError(j)
        lo, hi = 0, self.n - 1  # @line L11
        while lo < hi:  # @line L12
            mid = (lo + hi) // 2  # @line L13
            if self.rank1(mid + 1) >= j:  # @line L14
                hi = mid
            else:
                lo = mid + 1
        return lo  # @line L15

    def check_invariant(self) -> None:
        b, s = self.b, self.s
        c = 0
        for k in range(len(self.X)):
            if k % s == 0:
                assert self.RS[k // s] == c
            assert 0 <= self.RB[k] == c - self.RS[k // s] < s * b
            assert 0 <= self.X[k] < (1 << b)
            c += self.X[k].bit_count()
        assert c == self.ones
test_jacobson_rank.py 58 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/jacobson-rank/. The authors are listed in prov/D62R54.jsonl.

import itertools
import random

import pytest

from jacobson_rank import JacobsonRank


def check_all(b: list[int]) -> None:
    r = JacobsonRank(b)
    r.check_invariant()
    pref = list(itertools.accumulate(b, initial=0))
    assert len(r) == len(b)
    for i in range(len(b) + 1):
        assert r.rank1(i) == pref[i]
        assert r.rank0(i) == i - pref[i]
    for i in range(len(b)):
        assert r.access(i) == b[i]
    ones = [i for i, x in enumerate(b) if x]
    for j, p in enumerate(ones, 1):
        assert r.select1(j) == p


def test_page_example() -> None:
    b = [int(c) for c in "1011001011000111"]
    r = JacobsonRank(b)
    assert (r.b, r.s) == (2, 4)
    assert r.X == [1, 3, 0, 1, 3, 0, 2, 3, 0]
    assert r.RB == [0, 1, 3, 3, 0, 2, 2, 3, 0]
    assert r.RS == [0, 4, 9]
    assert r.rank1(13) == 6
    assert r.rank1(16) == 9
    assert r.select1(5) == 8
    check_all(b)


def test_edges() -> None:
    check_all([])
    check_all([1])
    check_all([0])
    check_all([1] * 64)
    check_all([0] * 33)
    check_all([0] * 100 + [1])
    with pytest.raises(IndexError):
        JacobsonRank([0, 0]).select1(1)
    with pytest.raises(IndexError):
        JacobsonRank([1]).rank1(2)


def test_random_against_prefix_sums() -> None:
    rng = random.Random(1989)
    for _ in range(600):
        n = rng.randrange(0, 400)
        p = rng.random()
        check_all([1 if rng.random() < p else 0 for _ in range(n)])

What the next level needs

  • the tests did not run in this build

Read the full page about Jacobson's rank structure