Algorithm Search⌘K

Bitset adjacency matrix in Python

Adjacency matrix stored as one bit per entry, using word-parallel operations.

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


class BitsetAdjacencyMatrix:
    """A graph on 0, ..., n - 1 whose matrix rows are packed into w-bit words.

    Bit v % w of word rows[u][v // w] is 1 when (u, v) is an edge. adjacent,
    add_edge and remove_edge take O(1) word operations; neighbors takes
    O(n/w + d(u)); degree and common take O(n/w). The space is n * ceil(n/w)
    words.
    """

    def __init__(self, n: int, directed: bool = False, w: int = 64) -> None:
        if n < 0 or w < 1:
            raise ValueError("need n >= 0 and w >= 1")
        self.n = n
        self.w = w
        self.directed = directed
        words = -(-n // w)
        self.rows: list[list[int]] = [[0] * words for _ in range(n)]  # @line L1

    def _check(self, u: int, v: int) -> None:
        if not (0 <= u < self.n and 0 <= v < self.n):
            raise IndexError("vertex out of range")

    def add_edge(self, u: int, v: int) -> None:
        self._check(u, v)
        if not self.directed and u == v:
            raise ValueError("an undirected graph has no self-loops")
        w = self.w
        self.rows[u][v // w] |= 1 << (v % w)  # @line L2
        if not self.directed:
            self.rows[v][u // w] |= 1 << (u % w)  # @line L3

    def remove_edge(self, u: int, v: int) -> None:
        self._check(u, v)
        w = self.w
        self.rows[u][v // w] &= ~(1 << (v % w))  # @line L4
        if not self.directed:
            self.rows[v][u // w] &= ~(1 << (u % w))  # @line L5

    def adjacent(self, u: int, v: int) -> bool:
        self._check(u, v)
        w = self.w
        return (self.rows[u][v // w] >> (v % w)) & 1 == 1  # @line L6

    def neighbors(self, u: int) -> list[int]:
        self._check(u, u)
        out = []
        for j, word in enumerate(self.rows[u]):  # @line L7
            x = word  # @line L8
            while x:  # @line L9
                out.append(j * self.w + (x & -x).bit_length() - 1)  # @line L10
                x &= x - 1  # @line L11
        return out

    def degree(self, u: int) -> int:
        self._check(u, u)
        return sum(x.bit_count() for x in self.rows[u])  # @line L12

    def common(self, u: int, v: int) -> int:
        self._check(u, v)
        return sum((a & b).bit_count() for a, b in zip(self.rows[u], self.rows[v]))  # @line L13

    def add_vertex(self) -> int:
        if self.n % self.w == 0:
            for row in self.rows:
                row.append(0)  # @line L14
        self.rows.append([0] * (-(-(self.n + 1) // self.w)))  # @line L15
        self.n += 1
        return self.n - 1

    def edges(self) -> list[tuple[int, int]]:
        return [
            (u, v)
            for u in range(self.n)
            for v in self.neighbors(u)
            if self.directed or u < v
        ]

    def complement(self) -> "BitsetAdjacencyMatrix":
        """The complement graph: NOT of each word, then clear padding and, if undirected, the diagonal."""
        c = BitsetAdjacencyMatrix(self.n, self.directed, self.w)
        full = (1 << self.w) - 1
        r = self.n % self.w
        last = full if r == 0 else (1 << r) - 1
        for u in range(self.n):
            row = [~x & full for x in self.rows[u]]
            if row:
                row[-1] &= last
            if not self.directed:
                row[u // self.w] &= ~(1 << (u % self.w))
            c.rows[u] = row
        return c

    def check_invariant(self) -> None:
        words = -(-self.n // self.w)
        assert len(self.rows) == self.n
        r = self.n % self.w
        for u in range(self.n):
            row = self.rows[u]
            assert len(row) == words
            for x in row:
                assert 0 <= x < (1 << self.w)
            if row and r:
                assert row[-1] >> r == 0
            if not self.directed:
                assert not self.adjacent(u, u)
                for v in self.neighbors(u):
                    assert self.adjacent(v, u)
test_bitset_adjacency_matrix.py 110 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/bitset-adjacency-matrix/. The authors are listed in prov/WJ5KV3.jsonl.

import random

import pytest

from bitset_adjacency_matrix import BitsetAdjacencyMatrix

EDGES = [(0, 1), (0, 2), (1, 2), (1, 3), (2, 3), (3, 4), (4, 5)]


def test_example() -> None:
    g = BitsetAdjacencyMatrix(6, w=4)
    for u, v in EDGES:
        g.add_edge(u, v)
    assert g.rows == [[0b0110, 0], [0b1101, 0], [0b1011, 0], [0b0110, 1], [0b1000, 2], [0, 1]]
    assert sum(g.common(u, v) for u, v in EDGES) == 6
    assert g.adjacent(1, 3)
    assert g.neighbors(3) == [1, 2, 4]
    assert g.degree(1) == 3
    assert g.common(1, 2) == 2
    g.remove_edge(2, 3)
    assert g.rows[2][0] == 0b0011 and g.rows[3][0] == 0b0010
    assert g.add_vertex() == 6
    assert all(len(r) == 2 for r in g.rows)
    g.check_invariant()


def test_complement_and_growth() -> None:
    g = BitsetAdjacencyMatrix(6, w=4)
    for u, v in EDGES:
        g.add_edge(u, v)
    c = g.complement()
    c.check_invariant()
    assert c.edges() == [(0, 3), (0, 4), (0, 5), (1, 4), (1, 5), (2, 4), (2, 5), (3, 5)]
    h = BitsetAdjacencyMatrix(4, w=4)
    assert h.add_vertex() == 4
    assert all(len(r) == 2 for r in h.rows)
    h.add_edge(4, 0)
    assert h.neighbors(0) == [4]
    h.check_invariant()


def test_edge_cases() -> None:
    g = BitsetAdjacencyMatrix(0)
    assert g.edges() == [] and g.complement().edges() == []
    assert g.add_vertex() == 0
    assert g.neighbors(0) == [] and g.degree(0) == 0
    with pytest.raises(ValueError):
        g.add_edge(0, 0)
    with pytest.raises(ValueError):
        BitsetAdjacencyMatrix(3, w=0)
    d = BitsetAdjacencyMatrix(1, directed=True, w=1)
    d.add_edge(0, 0)
    assert d.adjacent(0, 0) and d.neighbors(0) == [0]
    with pytest.raises(IndexError):
        d.adjacent(0, 1)


@pytest.mark.parametrize("w", [1, 3, 4, 64])
@pytest.mark.parametrize("directed", [False, True])
def test_random_against_set_model(directed: bool, w: int) -> None:
    rng = random.Random(5300 + w)
    for _ in range(150):
        n = rng.randint(1, 9)
        g = BitsetAdjacencyMatrix(n, directed, w)
        model: set[tuple[int, int]] = set()

        def key(a: int, b: int) -> tuple[int, int]:
            return (a, b) if directed else (min(a, b), max(a, b))

        for _ in range(rng.randint(0, 40)):
            op = rng.random()
            u, v = rng.randrange(n), rng.randrange(n)
            if op < 0.05:
                assert g.add_vertex() == n
                n += 1
            elif op < 0.5:
                if not directed and u == v:
                    continue
                g.add_edge(u, v)
                model.add(key(u, v))
            elif op < 0.75:
                g.remove_edge(u, v)
                model.discard(key(u, v))
            else:
                assert g.adjacent(u, v) == (key(u, v) in model)
            g.check_invariant()
        assert set(g.edges()) == model

        def nb(x: int) -> set[int]:
            s = {b for a, b in model if a == x}
            if not directed:
                s |= {a for a, b in model if b == x}
            return s

        for u in range(n):
            assert g.neighbors(u) == sorted(nb(u))
            assert g.degree(u) == len(nb(u))
            v = rng.randrange(n)
            assert g.common(u, v) == len(nb(u) & nb(v))
        c = g.complement()
        c.check_invariant()
        for u in range(n):
            for v in range(n):
                if not directed and u == v:
                    assert not c.adjacent(u, v)
                else:
                    assert c.adjacent(u, v) != g.adjacent(u, v)

What the next level needs

  • the tests did not run in this build

Read the full page about Bitset adjacency matrix