Algorithm Search⌘K

Bit vector in Python

A static sequence of bits that supports access, rank and select queries.

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

from collections.abc import Iterable

W = 64  # bits in a word
MASK = (1 << W) - 1


class BitVector:
    """A static bit vector packed into 64-bit words, with access, rank and select.

    Bit i is bit i mod 64 of word i // 64, least significant bit first.
    Bits past the end of the last word are 0.
    Cost: build O(n), access O(1), rank and select O(n / 64 + 64).
    """

    def __init__(self, bits: Iterable[int] = ()) -> None:
        b = list(bits)
        self.n = len(b)
        self.words = [0] * ((self.n + W - 1) // W)  # @line L1
        for i in range(self.n):  # @line L2
            if b[i]:  # @line L3
                self.words[i // W] |= 1 << (i % W)
        self.ones = sum(x.bit_count() for x in self.words)

    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  # @line L4

    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)
        q, r, c = i // W, i % W, 0  # @line L5
        for k in range(q):  # @line L6
            c += self.words[k].bit_count()
        if r > 0:  # @line L7
            c += (self.words[q] & ((1 << r) - 1)).bit_count()
        return c  # @line L8

    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, b: int) -> int:
        x = self.words[k]
        if b == 1:
            return x
        x = ~x & MASK
        if k == len(self.words) - 1 and self.n % W:
            x &= (1 << (self.n % W)) - 1  # mask the padding
        return x

    def _select(self, j: int, b: int) -> int:
        total = self.ones if b == 1 else self.n - self.ones
        if not 1 <= j <= total:
            raise IndexError(j)
        k = 0  # @line L9
        while self._word(k, b).bit_count() < j:  # @line L10
            j -= self._word(k, b).bit_count()
            k += 1
        x = self._word(k, b)  # @line L11
        for _ in range(j - 1):  # @line L12
            x &= x - 1
        return k * W + ((x & -x).bit_length() - 1)  # @line L13

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

    def select0(self, j: int) -> int:
        """Return the position of the j-th 0, for 1 <= j <= rank0(n)."""
        return self._select(j, 0)

    def check_invariant(self) -> None:
        assert len(self.words) == (self.n + W - 1) // W
        if self.words and self.n % W:
            assert self.words[-1] >> (self.n % W) == 0
        assert all(0 <= x <= MASK for x in self.words)
test_bit_vector.py 68 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/bit-vector/. The authors are listed in prov/6G48XR.jsonl.

import random

import pytest

from bit_vector import BitVector


def oracle(b: list[int]) -> tuple[list[int], list[int], list[int]]:
    rank = [0]
    for x in b:
        rank.append(rank[-1] + x)
    return rank, [i for i, x in enumerate(b) if x], [i for i, x in enumerate(b) if not x]


def check_all(b: list[int]) -> None:
    v = BitVector(b)
    v.check_invariant()
    rank, ones, zeros = oracle(b)
    assert len(v) == len(b)
    for i in range(len(b)):
        assert v.access(i) == b[i]
    for i in range(len(b) + 1):
        assert v.rank1(i) == rank[i]
        assert v.rank0(i) == i - rank[i]
    for j, p in enumerate(ones, 1):
        assert v.select1(j) == p
    for j, p in enumerate(zeros, 1):
        assert v.select0(j) == p


def test_page_example() -> None:
    b = [int(c) for c in "1011001011000111"]
    v = BitVector(b)
    assert v.access(9) == 1
    assert v.rank1(13) == 6
    assert v.rank0(13) == 7
    assert v.select1(7) == 13
    assert v.select0(4) == 7
    assert v.rank1(v.select1(7)) == 6
    with pytest.raises(IndexError):
        v.select1(10)
    check_all(b)


def test_edges() -> None:
    check_all([])
    check_all([0])
    check_all([1])
    check_all([1] * 64)
    check_all([0] * 64)
    check_all([1] * 128 + [0])
    check_all([0] * 130)
    check_all([0] * 63 + [1])
    v = BitVector([1] * 64)
    assert v.rank1(64) == 64
    with pytest.raises(IndexError):
        v.select0(1)


def test_random_against_oracle() -> None:
    rng = random.Random(6048)
    for _ in range(600):
        n = rng.randrange(0, 300)
        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 Bit vector