Greedy MST algorithm in Python
General scheme that builds a minimum spanning tree by repeatedly coloring the lightest edge of a cut that has no colored crossing edge.
- File
greedy_mst_algorithm.py, 103 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/greedy-mst-algorithm/. The authors are listed in prov/701F07.jsonl.
import random
from collections import deque
UNCOLORED, BLUE, RED = 0, 1, 2
def greedy_mst(
n: int, edges: list[tuple[int, int, float]], rng: random.Random | None = None
) -> tuple[list[int], list[tuple[str, int]]]:
"""Color the edges by the blue rule and the red rule; return (blue edges, log).
The vertices are 0 .. n - 1 and edges[i] = (u, v, w) is an undirected
edge. Weights may tie (the rules then pick by the edge index); the graph
may be disconnected, and parallel edges and self-loops are allowed. The
blue edges at the end form a minimum spanning forest. The log lists each
step as ("blue", i) or ("red", i).
With rng = None the choices are fixed: the uncolored edge with the
smallest index, the cut around the blue tree of its first end, and the
cycle through the blue path. With an rng, the edge, the cut (a random
union of blue trees) and the cycle (a random path of non-red edges) are
random, which exercises the free choices of the method.
Cost: m steps of O(n + m) each, so O(m (n + m)) time; O(n + m) space.
"""
m = len(edges)
color = [UNCOLORED] * m # @line L1
log: list[tuple[str, int]] = []
while UNCOLORED in color: # @line L2
todo = [i for i in range(m) if color[i] == UNCOLORED]
e = todo[0] if rng is None else rng.choice(todo)
u, v, _ = edges[e]
comp = _labels(n, edges, color)
if comp[u] != comp[v]: # @line L3
s = {comp[u]} # @line L4
if rng is not None:
s |= {t for t in set(comp) if t != comp[v] and rng.random() < 0.5}
crossing = [i for i in todo if (comp[edges[i][0]] in s) != (comp[edges[i][1]] in s)]
pick = min(crossing, key=lambda i: (edges[i][2], i)) # @line L5
color[pick] = BLUE
log.append(("blue", pick))
else: # @line L6
cycle = [e] + _path(n, edges, color, e, u, v, rng) # @line L7
pick = max((i for i in cycle if color[i] == UNCOLORED), key=lambda i: (edges[i][2], -i)) # @line L8
color[pick] = RED
log.append(("red", pick))
return [i for i in range(m) if color[i] == BLUE], log # @line L9
def _labels(n: int, edges: list[tuple[int, int, float]], color: list[int]) -> list[int]:
"""Label each vertex with the smallest vertex of its blue tree."""
adj: list[list[int]] = [[] for _ in range(n)]
for i, (a, b, _) in enumerate(edges):
if color[i] == BLUE:
adj[a].append(b)
adj[b].append(a)
comp = [-1] * n
for s in range(n):
if comp[s] < 0:
comp[s] = s
stack = [s]
while stack:
x = stack.pop()
for y in adj[x]:
if comp[y] < 0:
comp[y] = s
stack.append(y)
return comp
def _path(
n: int, edges: list[tuple[int, int, float]], color: list[int], e: int, u: int, v: int, rng: random.Random | None
) -> list[int]:
"""Edge indices of a path from u to v that avoids e and red edges.
Without an rng, only blue edges are used (such a path exists when u and v
are in one blue tree). With an rng, any non-red edges, in random order.
"""
adj: list[list[tuple[int, int]]] = [[] for _ in range(n)]
for i, (a, b, _) in enumerate(edges):
if i != e and (color[i] == BLUE or (rng is not None and color[i] == UNCOLORED)):
adj[a].append((b, i))
adj[b].append((a, i))
if rng is not None:
for lst in adj:
rng.shuffle(lst)
back: dict[int, tuple[int, int]] = {u: (-1, -1)}
queue = deque([u])
while queue and v not in back:
x = queue.popleft()
for y, i in adj[x]:
if y not in back:
back[y] = (x, i)
queue.append(y)
path: list[int] = []
x = v
while x != u:
x, i = back[x]
path.append(i)
return path
test_greedy_mst_algorithm.py 70 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/greedy-mst-algorithm/. The authors are listed in prov/701F07.jsonl.
import itertools
import random
from greedy_mst_algorithm import greedy_mst
EXAMPLE = [(0, 1, 4), (0, 2, 3), (1, 2, 1), (1, 3, 2), (2, 3, 4), (2, 4, 5), (3, 4, 7), (3, 5, 6), (4, 5, 2)]
def components(n, edges, chosen):
parent = list(range(n))
def find(x):
while parent[x] != x:
x = parent[x]
return x
for i in chosen:
u, v, _ = edges[i]
parent[find(u)] = find(v)
return len({find(x) for x in range(n)})
def is_forest(n, edges, chosen):
return components(n, edges, chosen) == n - len(chosen)
def brute_force(n, edges):
k = n - components(n, edges, range(len(edges)))
best = None
for s in itertools.combinations(range(len(edges)), k):
if is_forest(n, edges, s):
w = sum(edges[i][2] for i in s)
best = w if best is None or w < best else best
return best
def test_example():
blue, log = greedy_mst(6, EXAMPLE)
assert log == [
("blue", 1), ("blue", 2), ("red", 0), ("blue", 3), ("red", 4),
("blue", 5), ("red", 6), ("blue", 8), ("red", 7),
]
assert blue == [1, 2, 3, 5, 8]
def test_edge_cases():
assert greedy_mst(0, []) == ([], [])
assert greedy_mst(1, [(0, 0, 4)]) == ([], [("red", 0)])
assert greedy_mst(2, [(0, 1, 3), (0, 1, 1)]) == ([1], [("blue", 1), ("red", 0)])
blue, _ = greedy_mst(3, [(0, 1, 1), (1, 2, 1), (2, 0, 1)])
assert len(blue) == 2
blue, _ = greedy_mst(4, [(0, 1, 2), (2, 3, 5)])
assert blue == [0, 1]
def test_against_brute_force_with_random_choices():
rng = random.Random(1983)
for _ in range(600):
n = rng.randrange(1, 7)
m = rng.randrange(0, 9)
edges = [(rng.randrange(n), rng.randrange(n), rng.randrange(0, 3)) for _ in range(m)]
blue, log = greedy_mst(n, edges, rng)
assert len(log) == m
assert sorted(i for _, i in log) == list(range(m))
assert is_forest(n, edges, blue)
assert components(n, edges, blue) == components(n, edges, range(m))
assert sum(edges[i][2] for i in blue) == brute_force(n, edges)
What the next level needs
- the tests did not run in this build