Generic component-growing algorithm in Python
Builds the connected component of a start vertex by repeatedly adding any vertex adjacent to the current set, the common core of BFS and DFS.
- File
generic_component_growing.py, 66 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/generic-component-growing/. The authors are listed in prov/753N4R.jsonl.
from typing import Callable
Pick = Callable[[int], int]
def _last(size: int) -> int:
return size - 1
def grow(
adj: list[list[int]],
s: int,
k: int,
comp: list[int | None],
parent: list[int | None],
pick: Pick = _last,
) -> None:
"""Give the label k to every vertex that is reachable from s and has no label yet.
Precondition: adj is the adjacency lists of an undirected graph on 0 .. n-1, with
each edge {u, v} in the lists of both ends; parallel edges and loops are allowed.
comp[s] is None. pick(size) returns any index in range(size): it chooses which
vertex of the frontier is scanned next. Sets parent[v] for each new vertex except s.
Time O(n_s + m_s) for the component of s if pick is O(1); extra space O(n_s).
"""
nxt: dict[int, int] = {s: 0}
comp[s] = k # @line L1
frontier = [s] # @line L2
while frontier: # @line L3
j = pick(len(frontier)) # @line L4
u = frontier[j]
if nxt[u] == len(adj[u]): # @line L5
frontier[j] = frontier[-1] # swap with the last item, then pop: O(1)
frontier.pop()
continue
v = adj[u][nxt[u]] # @line L6
nxt[u] += 1
if comp[v] is None: # @line L7
comp[v] = k # @line L8
parent[v] = u
nxt[v] = 0
frontier.append(v)
def components(
adj: list[list[int]], pick: Pick = _last
) -> tuple[list[int | None], list[int | None], int]:
"""Label the connected components of an undirected graph.
Precondition: as for grow. Returns (comp, parent, k): comp[v] in 0 .. k-1 is the
label of the component of v, labels are given in the order of the smallest vertex
of each component, and the parent pointers form a spanning forest whose roots are
the smallest vertices of the components. Time O(n + m), extra space O(n).
"""
n = len(adj)
comp: list[int | None] = [None] * n # @line L9
parent: list[int | None] = [None] * n
k = 0
for s in range(n): # @line L10
if comp[s] is None: # @line L11
grow(adj, s, k, comp, parent, pick)
k += 1
return comp, parent, k # @line L12
test_generic_component_growing.py 103 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/generic-component-growing/. The authors are listed in prov/753N4R.jsonl.
import random
from generic_component_growing import components
def build(n: int, edges: list[tuple[int, int]]) -> list[list[int]]:
adj: list[list[int]] = [[] for _ in range(n)]
for u, v in edges:
adj[u].append(v)
if u != v:
adj[v].append(u)
return adj
def oracle(n: int, edges: list[tuple[int, int]]) -> list[int]:
"""Labels by repeated relaxation of the reachability relation."""
reach = [[u == v for v in range(n)] for u in range(n)]
for u, v in edges:
reach[u][v] = reach[v][u] = True
for w in range(n):
for u in range(n):
if reach[u][w]:
for v in range(n):
if reach[w][v]:
reach[u][v] = True
label: list[int] = [-1] * n
k = 0
for s in range(n):
if label[s] == -1:
for v in range(n):
if reach[s][v]:
label[v] = k
k += 1
return label
def check(n: int, edges: list[tuple[int, int]], pick=None) -> None:
adj = build(n, edges)
comp, parent, k = components(adj) if pick is None else components(adj, pick)
want = oracle(n, edges)
assert comp == want
assert k == (max(want) + 1 if n else 0)
roots = 0
for v in range(n):
p = parent[v]
if p is None:
roots += 1
assert all(comp[u] != comp[v] for u in range(v)) # the root is the smallest
else:
assert v in adj[p] and comp[p] == comp[v]
seen = set()
x: int | None = v
while x is not None: # the parent pointers have no cycle
assert x not in seen
seen.add(x)
x = parent[x]
assert roots == k
def test_example() -> None:
edges = [(0, 1), (1, 2), (2, 0), (3, 4), (3, 4), (5, 6), (6, 7), (5, 7), (8, 8)]
comp, parent, k = components(build(9, edges))
assert comp == [0, 0, 0, 1, 1, 2, 2, 2, 3]
assert k == 4
assert sum(p is not None for p in parent) == 9 - 4
check(9, edges)
check(9, edges, pick=lambda size: 0)
def test_edges() -> None:
assert components([]) == ([], [], 0)
check(1, [])
check(1, [(0, 0)])
check(1, [(0, 0), (0, 0)])
check(2, [(0, 1), (0, 1), (1, 0)])
check(5, [])
check(4, [(0, 3), (3, 3)])
def test_long_path() -> None:
n = 20000
edges = [(i, i + 1) for i in range(n - 1)]
comp, parent, k = components(build(n, edges))
assert k == 1 and comp == [0] * n
def test_random() -> None:
rng = random.Random(753)
for t in range(600):
n = rng.randint(0, 9)
m = rng.randint(0, 12) if n else 0
edges = [(rng.randrange(n), rng.randrange(n)) for _ in range(m)]
mode = t % 3
if mode == 0:
check(n, edges)
elif mode == 1:
check(n, edges, pick=lambda size: 0)
else:
r = random.Random(t)
check(n, edges, pick=lambda size: r.randrange(size))
What the next level needs
- the tests did not run in this build
Read the full page about Generic component-growing algorithm