Algorithm Search⌘K

Dynamic bit vector in Python

A bit vector that supports insertions and deletions along with rank and select, usually in O(log n) time.

File
dynamic_bit_vector.py, 235 lines
Level
I0 not checked yet
Toolchain
Python 3.14, pytest 9
Test framework
pytest
Run the tests
python3 -m pytest
dynamic_bit_vector.pyRawGitHub
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/dynamic-bit-vector/. The authors are listed in prov/ZZJ07J.jsonl.
"""Dynamic bit vector: rank, select, insert and delete in O(log n) expected time.

The bits are cut into chunks of at most 2B bits. The chunks, in order,
are the nodes of an implicit treap; each node also stores the number of
bits and of 1s in its subtree. A query walks down from the root and
finishes inside one chunk; an update changes one chunk, fixes the counts
on the path, and splits a chunk that grows past 2B bits.
"""

from __future__ import annotations

import random
from typing import Optional


class _Node:
    __slots__ = ("chunk", "pri", "left", "right", "size", "ones")

    def __init__(self, chunk: list[int], pri: float) -> None:
        self.chunk = chunk
        self.pri = pri
        self.left: Optional[_Node] = None
        self.right: Optional[_Node] = None
        self.size = len(chunk)
        self.ones = sum(chunk)


def _size(t: Optional[_Node]) -> int:
    return t.size if t else 0


def _ones(t: Optional[_Node]) -> int:
    return t.ones if t else 0


def _chunk_ones(t: _Node) -> int:
    """Number of 1s in the node's own chunk."""
    return t.ones - _ones(t.left) - _ones(t.right)


def _pull(t: _Node) -> None:
    t.size = _size(t.left) + len(t.chunk) + _size(t.right)
    t.ones = _ones(t.left) + sum(t.chunk) + _ones(t.right)


def _split(t: Optional[_Node], k: int) -> tuple[Optional[_Node], Optional[_Node]]:
    """Split at bit k, which must be a chunk boundary: left has bits [0, k)."""
    if t is None:
        return None, None
    ls = _size(t.left)
    if k <= ls:
        a, b = _split(t.left, k)
        t.left = b
        _pull(t)
        return a, t
    a, b = _split(t.right, k - ls - len(t.chunk))
    t.right = a
    _pull(t)
    return t, b


def _merge(a: Optional[_Node], b: Optional[_Node]) -> Optional[_Node]:
    if a is None:
        return b
    if b is None:
        return a
    if a.pri > b.pri:
        a.right = _merge(a.right, b)
        _pull(a)
        return a
    b.left = _merge(a, b.left)
    _pull(b)
    return b


def _pop_leftmost(t: _Node) -> Optional[_Node]:
    if t.left is None:
        return t.right
    t.left = _pop_leftmost(t.left)
    _pull(t)
    return t


class DynamicBitVector:
    """A bit vector with access, rank, select, insert, delete and set."""

    def __init__(self, bits: Optional[list[int]] = None, block: int = 64, seed: int = 0) -> None:
        """Build from ``bits``; chunks hold between 1 and 2 * ``block`` bits."""
        self.B = block
        self.rng = random.Random(seed)
        self.root: Optional[_Node] = None
        bits = list(bits or [])
        for s in range(0, len(bits), block):
            self.root = _merge(self.root, _Node(bits[s:s + block], self.rng.random()))

    def __len__(self) -> int:
        return _size(self.root)

    def to_list(self) -> list[int]:
        """All bits, in order."""
        out: list[int] = []

        def walk(t: Optional[_Node]) -> None:
            if t:
                walk(t.left)
                out.extend(t.chunk)
                walk(t.right)

        walk(self.root)
        return out

    def _find(self, i: int) -> tuple[list[_Node], int, int]:
        """Path to the node holding bit i (or the last node if i = n), offset, start."""
        path: list[_Node] = []
        x, start = self.root, 0
        while x is not None:
            path.append(x)
            ls = _size(x.left)
            if i < ls:
                x = x.left
            elif i < ls + len(x.chunk) or (x.right is None and i == ls + len(x.chunk)):
                return path, i - ls, start + ls
            else:
                i -= ls + len(x.chunk)
                start += ls + len(x.chunk)
                x = x.right
        raise IndexError("position out of range")

    def access(self, i: int) -> int:
        """The bit at position i."""
        if not 0 <= i < len(self):
            raise IndexError("position out of range")
        path, off, _ = self._find(i)
        return path[-1].chunk[off]

    def rank1(self, i: int) -> int:
        """Number of 1s in positions 0..i-1, for 0 <= i <= n."""
        x, r = self.root, 0  # @line L1
        while x is not None:  # @line L2
            ls = _size(x.left)
            if i < ls:  # @line L3
                x = x.left
            elif i <= ls + len(x.chunk):  # @line L4
                return r + _ones(x.left) + sum(x.chunk[: i - ls])
            else:  # @line L5
                r += _ones(x.left) + _chunk_ones(x)
                i -= ls + len(x.chunk)
                x = x.right
        return r  # @line L6

    def rank0(self, i: int) -> int:
        """Number of 0s in positions 0..i-1."""
        return i - self.rank1(i)

    def select1(self, j: int) -> int:
        """Position of the j-th 1, for 1 <= j <= rank1(n)."""
        if not 1 <= j <= _ones(self.root):
            raise IndexError("no such 1")
        x, base = self.root, 0  # @line L7
        while x is not None:  # @line L8
            lo, ls = _ones(x.left), _size(x.left)
            co = _chunk_ones(x)
            if j <= lo:  # @line L9
                x = x.left
            elif j <= lo + co:  # @line L10
                j -= lo
                for k, b in enumerate(x.chunk):
                    j -= b
                    if j == 0:
                        return base + ls + k
            else:  # @line L11
                j -= lo + co
                base += ls + len(x.chunk)
                x = x.right
        raise AssertionError("unreachable")

    def select0(self, j: int) -> int:
        """Position of the j-th 0, by binary search over rank0."""
        if not 1 <= j <= len(self) - _ones(self.root):
            raise IndexError("no such 0")
        lo, hi = 0, len(self) - 1
        while lo < hi:
            mid = (lo + hi) // 2
            if self.rank0(mid + 1) >= j:
                hi = mid
            else:
                lo = mid + 1
        return lo

    def insert(self, i: int, b: int) -> None:
        """Insert bit b before position i, for 0 <= i <= n."""
        if not 0 <= i <= len(self):
            raise IndexError("position out of range")
        if self.root is None:
            self.root = _Node([b], self.rng.random())
            return
        path, off, start = self._find(i)  # @line L12
        x = path[-1]
        x.chunk.insert(off, b)  # @line L13
        for y in reversed(path):  # @line L14
            _pull(y)
        if len(x.chunk) > 2 * self.B:  # @line L15
            half = len(x.chunk) // 2
            new = _Node(x.chunk[half:], self.rng.random())
            x.chunk = x.chunk[:half]
            for y in reversed(path):
                _pull(y)
            left, right = _split(self.root, start + half)
            self.root = _merge(_merge(left, new), right)

    def delete(self, i: int) -> int:
        """Remove and return the bit at position i."""
        if not 0 <= i < len(self):
            raise IndexError("position out of range")
        path, off, start = self._find(i)  # @line L16
        x = path[-1]
        b = x.chunk.pop(off)  # @line L17
        for y in reversed(path):  # @line L18
            _pull(y)
        if not x.chunk:  # @line L19
            left, right = _split(self.root, start)
            assert right is not None
            self.root = _merge(left, _pop_leftmost(right))
        return b

    def set(self, i: int, b: int) -> None:
        """Overwrite the bit at position i with b."""
        if not 0 <= i < len(self):
            raise IndexError("position out of range")
        path, off, _ = self._find(i)
        path[-1].chunk[off] = b
        for y in reversed(path):
            _pull(y)
test_dynamic_bit_vector.py 83 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/dynamic-bit-vector/. The authors are listed in prov/ZZJ07J.jsonl.
"""Tests for the dynamic bit vector."""

from __future__ import annotations

import random
import unittest

from dynamic_bit_vector import DynamicBitVector


def check(t: unittest.TestCase, d: DynamicBitVector, ref: list[int]) -> None:
    t.assertEqual(d.to_list(), ref)
    t.assertEqual(len(d), len(ref))
    ones = [i for i, b in enumerate(ref) if b]
    zeros = [i for i, b in enumerate(ref) if not b]
    for i in range(len(ref) + 1):
        t.assertEqual(d.rank1(i), sum(ref[:i]))
    for j, p in enumerate(ones, 1):
        t.assertEqual(d.select1(j), p)
    for j, p in enumerate(zeros, 1):
        t.assertEqual(d.select0(j), p)


class TestDynamicBitVector(unittest.TestCase):
    def test_example(self) -> None:
        d = DynamicBitVector([0, 1, 1, 0, 1, 0, 0, 1], block=2)
        self.assertEqual(d.rank1(5), 3)
        self.assertEqual(d.select1(3), 4)
        d.insert(2, 1)
        self.assertEqual(d.to_list(), [0, 1, 1, 1, 0, 1, 0, 0, 1])
        self.assertEqual(d.rank1(5), 3)
        self.assertEqual(d.select1(4), 5)
        self.assertEqual(d.delete(0), 0)
        self.assertEqual(d.to_list(), [1, 1, 1, 0, 1, 0, 0, 1])
        self.assertEqual(d.select1(4), 4)
        self.assertEqual(d.select0(1), 3)

    def test_empty_and_errors(self) -> None:
        d = DynamicBitVector()
        self.assertEqual(len(d), 0)
        self.assertEqual(d.rank1(0), 0)
        with self.assertRaises(IndexError):
            d.select1(1)
        with self.assertRaises(IndexError):
            d.delete(0)
        d.insert(0, 1)
        self.assertEqual(d.select1(1), 0)
        self.assertEqual(d.delete(0), 1)
        self.assertEqual(len(d), 0)
        d.insert(0, 0)
        self.assertEqual(d.to_list(), [0])

    def test_random_ops(self) -> None:
        rng = random.Random(707)
        for trial in range(6):
            block = rng.choice([1, 2, 3, 4, 8])
            ref = [rng.randint(0, 1) for _ in range(rng.randint(0, 20))]
            d = DynamicBitVector(ref, block=block, seed=trial)
            for _ in range(100):
                op = rng.random()
                if op < 0.45 or not ref:
                    i, b = rng.randint(0, len(ref)), rng.randint(0, 1)
                    d.insert(i, b)
                    ref.insert(i, b)
                elif op < 0.8:
                    i = rng.randrange(len(ref))
                    self.assertEqual(d.delete(i), ref.pop(i))
                else:
                    i, b = rng.randrange(len(ref)), rng.randint(0, 1)
                    d.set(i, b)
                    ref[i] = b
                self.assertEqual(len(d), len(ref))
                if ref:
                    i = rng.randrange(len(ref))
                    self.assertEqual(d.access(i), ref[i])
                    self.assertEqual(d.rank1(i), sum(ref[:i]))
            check(self, d, ref)


if __name__ == "__main__":
    unittest.main()

What the next level needs

  • the tests did not run in this build

Read the full page about Dynamic bit vector