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
# 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