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