Algorithm Search⌘K

Gomory-Hu tree in Python

A weighted tree encoding all pairwise min cut values of an undirected graph.

File
gomory_hu_tree.py, 143 lines
Level
I0 not checked yet
Toolchain
Python 3.14, pytest 9
Test framework
pytest
Run the tests
python3 -m pytest
gomory_hu_tree.pyRawGitHub
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/gomory-hu-tree/. The authors are listed in prov/85Y2JY.jsonl.

from collections import deque
from fractions import Fraction
from typing import Optional, Union

Number = Union[int, Fraction, float]


def _min_cut(n: int, edges: list[tuple[int, int, Number]], s: int, t: int) -> tuple[Number, set[int]]:
    """Minimum s-t cut of the undirected graph by shortest augmenting paths.

    Each undirected edge is two arcs u->v and v->u, each with its own residual pair.
    Returns (value, S) with S the vertices reachable from s in the final residual graph.
    """
    head: list[int] = []
    res: list[Number] = []
    adj: list[list[int]] = [[] for _ in range(n)]
    for u, v, w in edges:
        if u == v:
            continue
        for a, b in ((u, v), (v, u)):
            adj[a].append(len(head))
            head.append(b)
            res.append(w)
            adj[b].append(len(head))
            head.append(a)
            res.append(0)
    value: Number = 0
    while True:
        parent = [-1] * n
        parent[s] = -2
        queue = deque([s])
        while queue:
            u = queue.popleft()
            for a in adj[u]:
                v = head[a]
                if res[a] > 0 and parent[v] == -1:
                    parent[v] = a
                    queue.append(v)
        if parent[t] == -1:
            return value, {v for v in range(n) if parent[v] != -1}
        delta = None
        v = t
        while v != s:
            a = parent[v]
            delta = res[a] if delta is None else min(delta, res[a])
            v = head[a ^ 1]
        v = t
        while v != s:
            a = parent[v]
            res[a] -= delta
            res[a ^ 1] += delta
            v = head[a ^ 1]
        value += delta


class GomoryHuTree:
    """Gomory-Hu cut tree of an undirected graph, built by Gusfield's method.

    The graph has the vertices 0 .. n-1 and the undirected edges (u, v, w), w >= 0
    (int, Fraction or float). Parallel and zero-weight edges are allowed; self-loops
    are ignored. The tree is stored as parent[v] and weight[v] for v != 0, rooted at 0.
    Building costs n - 1 maximum flows on the whole graph; each query walks one tree
    path in O(n) time.
    """

    def __init__(self, n: int, edges: list[tuple[int, int, Number]]) -> None:
        for u, v, w in edges:
            if w < 0:
                raise ValueError("weights must be nonnegative")
            if not (0 <= u < n and 0 <= v < n):
                raise ValueError("vertex out of range")
        self.n = n
        self.parent: list[int] = [0] * n  # @line L1
        self.weight: list[Number] = [0] * n
        if n > 0:
            self.parent[0] = -1
        for s in range(1, n):  # @line L2
            t = self.parent[s]  # @line L3
            lam, side = _min_cut(n, edges, s, t)  # @line L4
            self.weight[s] = lam
            for v in range(n):  # @line L5
                if v != s and v in side and self.parent[v] == t:
                    self.parent[v] = s  # @line L6
            if self.parent[t] != -1 and self.parent[t] in side:  # @line L7
                self.parent[s] = self.parent[t]  # @line L8
                self.parent[t] = s
                self.weight[s] = self.weight[t]
                self.weight[t] = lam
        self.depth = [0] * n  # @line L9
        self.children: list[list[int]] = [[] for _ in range(n)]
        for v in range(1, n):
            self.children[self.parent[v]].append(v)
        order = [0] if n > 0 else []
        for v in order:
            for c in self.children[v]:
                self.depth[c] = self.depth[v] + 1
                order.append(c)

    def edges(self) -> list[tuple[int, int, Number]]:
        """The n - 1 tree edges (a, b, w) with a < b, sorted."""
        out = [(min(v, self.parent[v]), max(v, self.parent[v]), self.weight[v]) for v in range(1, self.n)]
        out.sort(key=lambda e: (e[0], e[1]))
        return out

    def _lightest(self, u: int, v: int) -> int:
        """The lower end of a lightest edge on the tree path from u to v (u != v)."""
        if u == v:
            raise ValueError("u and v must differ")
        best = -1
        while u != v:  # @line L10
            if self.depth[u] < self.depth[v]:
                u, v = v, u
            if best == -1 or self.weight[u] < self.weight[best]:  # @line L11
                best = u
            u = self.parent[u]  # @line L12
        return best

    def min_cut_value(self, u: int, v: int) -> Number:
        """The minimum u-v cut value of the graph."""
        return self.weight[self._lightest(u, v)]  # @line L13

    def min_cut(self, u: int, v: int) -> tuple[Number, list[int]]:
        """(value, side): a minimum u-v cut, side being the sorted vertex set holding u."""
        b = self._lightest(u, v)  # @line L14
        below = {b}
        stack = [b]
        while stack:
            x = stack.pop()
            for c in self.children[x]:
                below.add(c)
                stack.append(c)
        side = below if u in below else set(range(self.n)) - below
        return self.weight[b], sorted(side)

    def global_min_cut(self) -> Optional[tuple[Number, list[int]]]:
        """A global minimum cut (value, side), or None if n < 2."""
        if self.n < 2:
            return None
        b = min(range(1, self.n), key=lambda v: self.weight[v])  # @line L15
        return self.min_cut(b, self.parent[b])
test_gomory_hu_tree.py 121 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/gomory-hu-tree/. The authors are listed in prov/85Y2JY.jsonl.

import random
from fractions import Fraction
from itertools import combinations

import pytest

from gomory_hu_tree import GomoryHuTree

P, Q, R, S, U, V = range(6)
SIX = [(P, Q, 1), (P, R, 7), (Q, R, 1), (Q, S, 3), (Q, U, 2), (R, U, 4), (S, U, 1), (S, V, 6), (U, V, 2)]


def cut_value(edges, side):
    return sum(w for u, v, w in edges if (u in side) != (v in side))


def lam_brute(n, edges, a, b):
    """Oracle: the least cut over all vertex sets holding a and not b."""
    others = [v for v in range(n) if v not in (a, b)]
    best = None
    for k in range(len(others) + 1):
        for extra in combinations(others, k):
            c = cut_value(edges, {a, *extra})
            best = c if best is None else min(best, c)
    return best


def check(n, edges, tree):
    assert len(tree.edges()) == max(n - 1, 0)
    for a, b in combinations(range(n), 2):
        lam = lam_brute(n, edges, a, b)
        assert tree.min_cut_value(a, b) == lam
        assert tree.min_cut_value(b, a) == lam
        value, side = tree.min_cut(a, b)
        assert value == lam
        assert a in side and b not in side
        assert cut_value(edges, set(side)) == lam
    if n >= 2:
        value, side = tree.global_min_cut()
        assert 0 < len(side) < n
        assert cut_value(edges, set(side)) == value
        assert value == min(lam_brute(n, edges, a, b) for a, b in combinations(range(n), 2))


def test_six_vertex_example() -> None:
    tree = GomoryHuTree(6, SIX)
    assert tree.parent == [-1, U, P, Q, R, S]
    assert tree.weight[1:] == [7, 8, 6, 6, 8]
    assert tree.edges() == [(P, R, 8), (Q, S, 6), (Q, U, 7), (R, U, 6), (S, V, 8)]
    assert tree.min_cut_value(R, U) == 6
    assert tree.min_cut(R, U) == (6, [P, R])
    assert tree.global_min_cut() == (6, [S, V])
    check(6, SIX, tree)


def test_tiny() -> None:
    assert GomoryHuTree(0, []).edges() == []
    assert GomoryHuTree(1, [(0, 0, 5)]).global_min_cut() is None
    tree = GomoryHuTree(2, [(0, 1, 3), (1, 0, 2)])
    assert tree.edges() == [(0, 1, 5)]
    with pytest.raises(ValueError):
        tree.min_cut_value(1, 1)


def test_disconnected_and_zero_weight() -> None:
    edges = [(0, 1, 4), (2, 3, 0), (3, 4, 2)]
    tree = GomoryHuTree(5, edges)
    check(5, edges, tree)
    assert tree.min_cut_value(0, 3) == 0
    assert tree.min_cut_value(2, 3) == 0
    assert tree.global_min_cut()[0] == 0


def test_running_network_undirected() -> None:
    s, a, b, c, d, t = range(6)
    edges = [(s, a, 10), (s, c, 10), (a, b, 4), (a, c, 2), (a, d, 8), (c, d, 9), (d, b, 6), (b, t, 10), (d, t, 10)]
    tree = GomoryHuTree(6, edges)
    check(6, edges, tree)
    assert sorted(w for _, _, w in tree.edges()) == [20, 20, 20, 21, 21]


def test_fractions_and_floats() -> None:
    edges = [(0, 1, Fraction(1, 3)), (1, 2, Fraction(1, 2)), (0, 2, Fraction(1, 6))]
    check(3, edges, GomoryHuTree(3, edges))
    tree = GomoryHuTree(3, [(0, 1, 0.5), (1, 2, 0.25), (0, 2, 0.25)])
    assert sorted(w for _, _, w in tree.edges()) == [0.5, 0.75]


def test_errors() -> None:
    with pytest.raises(ValueError):
        GomoryHuTree(2, [(0, 1, -1)])
    with pytest.raises(ValueError):
        GomoryHuTree(2, [(0, 2, 1)])


def test_random_against_brute_force() -> None:
    rng = random.Random(8181)
    for _ in range(600):
        n = rng.randint(2, 7)
        edges = []
        for _ in range(rng.randint(0, 14)):
            edges.append((rng.randrange(n), rng.randrange(n), rng.choice([0, 1, 2, 3, 5, 8])))
        check(n, edges, GomoryHuTree(n, edges))


def test_parent_array_is_a_tree_rooted_at_0() -> None:
    rng = random.Random(31)
    for _ in range(100):
        n = rng.randint(1, 8)
        edges = [(rng.randrange(n), rng.randrange(n), rng.randint(0, 4)) for _ in range(10)]
        tree = GomoryHuTree(n, edges)
        for v in range(1, n):
            x, steps = v, 0
            while x != 0:
                x = tree.parent[x]
                steps += 1
                assert steps < n
            assert tree.depth[v] == steps

What the next level needs

  • the tests did not run in this build

Read the full page about Gomory-Hu tree