Incidence list in Python
Per-vertex list of the edges incident to it, rather than of neighbours.
- File
incidence_list.py, 102 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/incidence-list/. The authors are listed in prov/QV04NF.jsonl.
from typing import Iterator
class IncidenceList:
"""A graph stored as a table of edges and, for each vertex, the lists of its edges.
Edge e has a tail and a head. It sits in out_[tail] and in in_[head], and
pos_out[e], pos_in[e] give its index in those lists. In an undirected graph
the tail and head are only the order in which the ends were given, and the
edges of v are the edges of out_[v] followed by those of in_[v]. Parallel
edges and self-loops are allowed. Edge ids stay valid after other edges are
removed.
"""
def __init__(self, n: int = 0, directed: bool = False) -> None:
self.directed = directed
self.out_: list[list[int]] = [[] for _ in range(n)]
self.in_: list[list[int]] = [[] for _ in range(n)]
self.tail: list[int] = []
self.head: list[int] = []
self.weight: list[float] = []
self.pos_out: list[int] = []
self.pos_in: list[int] = []
self.alive: list[bool] = []
self.m = 0
def n(self) -> int:
return len(self.out_)
def add_vertex(self) -> int:
self.out_.append([])
self.in_.append([])
return len(self.out_) - 1
def add_edge(self, u: int, v: int, w: float = 1.0) -> int:
e = len(self.tail) # @line L1
self.tail.append(u)
self.head.append(v)
self.weight.append(w)
self.alive.append(True)
self.out_[u].append(e) # @line L2
self.pos_out.append(len(self.out_[u]) - 1)
self.in_[v].append(e) # @line L3
self.pos_in.append(len(self.in_[v]) - 1)
self.m += 1
return e # @line L4
def incident(self, v: int) -> Iterator[tuple[int, int]]:
"""Yield (edge, other end) for each edge of v. A self-loop is yielded twice when undirected."""
for e in self.out_[v]: # @line L5
yield e, self.head[e]
if not self.directed:
for e in self.in_[v]: # @line L6
yield e, self.tail[e]
def in_incident(self, v: int) -> Iterator[tuple[int, int]]:
"""Directed graphs: yield (edge, tail) for each edge that enters v."""
for e in self.in_[v]:
yield e, self.tail[e]
def degree(self, v: int) -> int:
"""Out-degree in a directed graph; degree (a loop counts 2) in an undirected one."""
if self.directed:
return len(self.out_[v])
return len(self.out_[v]) + len(self.in_[v])
@staticmethod
def _swap_remove(lst: list[int], pos: list[int], e: int) -> None:
i = pos[e] # @line L11
f = lst[-1]
lst[i] = f # @line L12
pos[f] = i
lst.pop() # @line L13
def remove_edge(self, e: int) -> None:
if not self.alive[e]:
raise KeyError(e)
u, v = self.tail[e], self.head[e] # @line L7
self._swap_remove(self.out_[u], self.pos_out, e) # @line L8
self._swap_remove(self.in_[v], self.pos_in, e) # @line L9
self.alive[e] = False # @line L10
self.m -= 1
def find_edge(self, u: int, v: int) -> int | None:
"""Return the id of some edge from u to v (between u and v if undirected), or None.
Directed: scan the shorter of out_[u] and in_[v]. Undirected: scan the
edges of whichever of u and v has the smaller degree.
"""
if self.directed:
if len(self.out_[u]) <= len(self.in_[v]):
return next((e for e in self.out_[u] if self.head[e] == v), None)
return next((e for e in self.in_[v] if self.tail[e] == u), None)
if self.degree(u) > self.degree(v):
u, v = v, u
return next((e for e, x in self.incident(u) if x == v), None)
def edges(self) -> list[tuple[int, int, int, float]]:
return [(e, self.tail[e], self.head[e], self.weight[e]) for e in range(len(self.tail)) if self.alive[e]]
test_incidence_list.py 67 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/incidence-list/. The authors are listed in prov/QV04NF.jsonl.
import random
from collections import Counter
from incidence_list import IncidenceList
def build_example() -> IncidenceList:
g = IncidenceList(4)
for u, v in [(0, 1), (1, 2), (0, 2), (1, 2), (2, 3), (3, 3)]:
g.add_edge(u, v)
return g
def test_example() -> None:
g = build_example()
assert [g.degree(v) for v in range(4)] == [2, 3, 4, 3]
assert sorted(g.incident(2)) == [(1, 1), (2, 0), (3, 1), (4, 3)]
assert sorted(g.incident(3)) == [(4, 2), (5, 3), (5, 3)]
g.remove_edge(1)
assert sorted(g.incident(1)) == [(0, 0), (3, 2)]
assert sorted(g.incident(2)) == [(2, 0), (3, 1), (4, 3)]
assert g.find_edge(2, 1) == 3
assert g.m == 5
assert sum(g.degree(v) for v in range(4)) == 2 * g.m
def test_directed() -> None:
g = IncidenceList(3, directed=True)
a = g.add_edge(0, 1)
g.add_edge(1, 2)
g.add_edge(2, 0)
assert list(g.incident(0)) == [(a, 1)]
assert list(g.in_incident(0)) == [(2, 2)]
assert g.find_edge(1, 0) is None
assert g.find_edge(0, 1) == a
def test_random_against_multiset() -> None:
rng = random.Random(7)
for directed in (False, True):
g = IncidenceList(6, directed=directed)
live: dict[int, tuple[int, int]] = {}
for _ in range(400):
if live and rng.random() < 0.4:
e = rng.choice(list(live))
g.remove_edge(e)
del live[e]
else:
u, v = rng.randrange(6), rng.randrange(6)
live[g.add_edge(u, v)] = (u, v)
for x in range(6):
want: Counter[tuple[int, int]] = Counter()
for e, (u, v) in live.items():
if u == x:
want[(e, v)] += 1
if not directed and v == x:
want[(e, u)] += 1
assert Counter(g.incident(x)) == want
assert g.m == len(live)
u, v = rng.randrange(6), rng.randrange(6)
hit = g.find_edge(u, v)
ok = {e for e, (a, b) in live.items() if (a, b) == (u, v) or (not directed and (b, a) == (u, v))}
assert (hit is None and not ok) or hit in ok
assert sorted(e for e, *_ in g.edges()) == sorted(live)
What the next level needs
- the tests did not run in this build