Algorithm Search⌘K

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
greedy_mst_algorithm.pyRawGitHub
# 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

Read the full page about Greedy MST algorithm