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