Algorithm Search⌘K

Network simplex method in Python

Simplex method specialized to min-cost flow, pivoting over spanning tree bases.

File
network_simplex.py, 139 lines
Level
I0 not checked yet
Toolchain
Python 3.14, pytest 9
Test framework
pytest
Run the tests
python3 -m pytest
network_simplex.pyRawGitHub
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/network-simplex/. The authors are listed in prov/SJBDWT.jsonl.

from collections import deque


def network_simplex(
    n: int,
    edges: list[tuple[int, int, float, float]],
    supply: list[float],
) -> tuple[float, list[float]] | None:
    """Minimum-cost flow by the primal network simplex method.

    Vertices are 0 .. n-1. Edges (u, v, cap, cost) are directed, with a
    capacity cap >= 0 and a cost of any sign, integers or real numbers.
    supply[v] is the net amount that must leave v (negative for a demand).
    Parallel and antiparallel edges are separate variables. Returns
    (cost, flow) with flow[i] on edges[i], or None if no feasible flow exists.

    The method keeps a strongly feasible spanning tree rooted at an artificial
    vertex, uses the most violating edge to enter (Dantzig's rule), and the
    last blocking edge of the cycle to leave (Cunningham's rule).
    """
    if len(supply) != n:
        raise ValueError("one supply per vertex")
    if sum(supply) != 0:
        return None
    m = len(edges)
    tail = [u for u, _, _, _ in edges]
    head = [v for _, v, _, _ in edges]
    cap = [c for _, _, c, _ in edges]
    cost = [k for _, _, _, k in edges]
    if any(c < 0 for c in cap):
        raise ValueError("capacities must be nonnegative")
    r = n
    big = (max([abs(k) for k in cost], default=0) + 1) * (n + 1)
    flow = [0] * m
    for v in range(n):  # @line L1
        if supply[v] >= 0:
            tail.append(v), head.append(r), flow.append(supply[v])
        else:
            tail.append(r), head.append(v), flow.append(-supply[v])
        cap.append(None)
        cost.append(big)
    in_tree = [False] * m + [True] * n
    while True:  # @line L2
        parent, pedge, depth, pi = tree_structure(n + 1, r, tail, head, cost, in_tree)  # @line L3
        enter, best = -1, 0
        for e in range(m):  # @line L4
            if in_tree[e] or cap[e] == 0:
                continue
            rc = cost[e] + pi[tail[e]] - pi[head[e]]
            violation = -rc if flow[e] == 0 else rc
            if violation > best:
                enter, best = e, violation
        if enter < 0:
            break
        if flow[enter] == 0:  # @line L5
            a, b = tail[enter], head[enter]
        else:
            a, b = head[enter], tail[enter]
        cycle = pivot_cycle(a, b, enter, flow[enter] == 0, parent, pedge, depth, tail)
        delta, leave = None, None
        for e, forward in cycle:  # @line L6
            res = residual(e, forward, flow, cap)
            if res is not None and (delta is None or res <= delta):
                delta, leave = res, e
        for e, forward in cycle:  # @line L7
            flow[e] += delta if forward else -delta
        if leave != enter:  # @line L8
            in_tree[leave] = False
            in_tree[enter] = True
    if any(flow[e] != 0 for e in range(m, m + n)):  # @line L9
        return None
    return sum(cost[e] * flow[e] for e in range(m)), flow[:m]  # @line L10


def residual(e, forward, flow, cap):
    """How much more flow edge e can take in the given direction; None for unbounded."""
    if forward:
        return None if cap[e] is None else cap[e] - flow[e]
    return flow[e]


def tree_structure(size, r, tail, head, cost, in_tree):
    """Parent, parent edge, depth and potentials of the spanning tree, rooted at r.

    The potentials make every tree edge (u, v) satisfy cost + pi[u] - pi[v] = 0.
    """
    adj = [[] for _ in range(size)]
    for e, t in enumerate(in_tree):
        if t:
            adj[tail[e]].append(e)
            adj[head[e]].append(e)
    parent = [-1] * size
    pedge = [-1] * size
    depth = [0] * size
    pi = [0] * size
    seen = [False] * size
    seen[r] = True
    queue = deque([r])
    while queue:
        u = queue.popleft()
        for e in adj[u]:
            w = head[e] if tail[e] == u else tail[e]
            if seen[w]:
                continue
            seen[w] = True
            parent[w], pedge[w], depth[w] = u, e, depth[u] + 1
            pi[w] = pi[u] + cost[e] if tail[e] == u else pi[u] - cost[e]
            queue.append(w)
    return parent, pedge, depth, pi


def pivot_cycle(a, b, enter, enter_forward, parent, pedge, depth, tail):
    """The cycle of the tree plus the entering edge, oriented a -> b along the entering edge.

    It is listed as (edge, forward) pairs in the order met when walking along
    the orientation from the apex: down to a, across to b, and up to the apex.
    forward means the walk goes from the tail of the edge to its head.
    """
    up_a, up_b = [], []
    x, y = a, b
    while x != y:
        if depth[x] >= depth[y]:
            up_a.append(x)
            x = parent[x]
        else:
            up_b.append(y)
            y = parent[y]
    cycle = []
    for v in reversed(up_a):  # walk from the apex down to a: parent[v] -> v
        e = pedge[v]
        cycle.append((e, tail[e] == parent[v]))
    cycle.append((enter, enter_forward))
    for v in up_b:  # walk from b up to the apex: v -> parent[v]
        e = pedge[v]
        cycle.append((e, tail[e] == v))
    return cycle
test_network_simplex.py 92 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/network-simplex/. The authors are listed in prov/SJBDWT.jsonl.

import itertools
import random
from fractions import Fraction

import pytest

from network_simplex import network_simplex

# s, a, b, t = 0 .. 3; edges (u, v, capacity, cost)
EXAMPLE = [(0, 1, 3, 1), (0, 2, 2, 4), (1, 2, 2, 1), (1, 3, 2, 5), (2, 3, 3, 1)]


def check(n, edges, supply, cost, flow):
    net = [0] * n
    for (u, v, c, k), x in zip(edges, flow):
        assert 0 <= x <= c
        net[u] += x
        net[v] -= x
    assert net == list(supply)
    assert cost == sum(k * x for (_, _, _, k), x in zip(edges, flow))


def brute(n, edges, supply):
    """Independent oracle: try every integral flow; integer data has an integral optimum."""
    best = None
    for flow in itertools.product(*(range(c + 1) for _, _, c, _ in edges)):
        net = [0] * n
        for (u, v, _, _), x in zip(edges, flow):
            net[u] += x
            net[v] -= x
        if net == list(supply):
            cost = sum(k * x for (_, _, _, k), x in zip(edges, flow))
            best = cost if best is None else min(best, cost)
    return best


def test_example_by_value() -> None:
    costs = []
    for d in range(6):
        cost, flow = network_simplex(4, EXAMPLE, [d, 0, 0, -d])
        check(4, EXAMPLE, [d, 0, 0, -d], cost, flow)
        costs.append(cost)
    assert costs == [0, 3, 6, 11, 17, 25]
    assert network_simplex(4, EXAMPLE, [6, 0, 0, -6]) is None


def test_zero_capacity_no_path_and_bad_input() -> None:
    assert network_simplex(2, [(0, 1, 0, -5)], [0, 0]) == (0, [0])
    assert network_simplex(2, [(0, 1, 0, 1)], [1, -1]) is None
    assert network_simplex(3, [(0, 1, 4, 1)], [1, 0, -1]) is None
    assert network_simplex(2, [(0, 1, 4, 1)], [1, 0]) is None
    with pytest.raises(ValueError):
        network_simplex(2, [(0, 1, -1, 1)], [0, 0])


def test_negative_cycle_antiparallel_and_loops() -> None:
    edges = [(0, 1, 2, -3), (1, 0, 3, 1), (0, 0, 4, -1), (1, 1, 2, 2)]
    cost, flow = network_simplex(2, edges, [0, 0])
    assert cost == -4 - 4
    assert flow == [2, 2, 4, 0]
    check(2, edges, [0, 0], cost, flow)


def test_real_data() -> None:
    edges = [(0, 1, Fraction(3, 2), Fraction(1, 3)), (0, 2, Fraction(1, 2), Fraction(-1, 7)), (2, 1, 2, Fraction(1, 5)), (1, 3, 3, 1)]
    supply = [Fraction(2), 0, 0, Fraction(-2)]
    cost, flow = network_simplex(4, edges, supply)
    assert flow == [Fraction(3, 2), Fraction(1, 2), Fraction(1, 2), 2]
    check(4, edges, supply, cost, flow)


def test_random_against_brute_force() -> None:
    rng = random.Random(77)
    for _ in range(600):
        n = rng.randint(2, 5)
        edges = [(rng.randrange(n), rng.randrange(n), rng.randint(0, 3), rng.randint(-4, 6)) for _ in range(rng.randint(0, 6))]
        supply = [0] * n
        for _ in range(rng.randint(0, 3)):
            u, v = rng.randrange(n), rng.randrange(n)
            supply[u] += 1
            supply[v] -= 1
        result = network_simplex(n, edges, supply)
        expected = brute(n, edges, supply)
        if expected is None:
            assert result is None
        else:
            cost, flow = result
            assert cost == expected
            check(n, edges, supply, cost, flow)

What the next level needs

  • the tests did not run in this build

Read the full page about Network simplex method