Algorithm Search⌘K

Malhotra-Pramodh Kumar-Maheshwari algorithm in Python

Maximum flow algorithm that builds a layered network and repeatedly pushes flow through the vertex of least potential, running in O(V^3).

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

from collections import deque


def mpm(n: int, edges: list[tuple[int, int, float]], s: int, t: int) -> tuple[float, list[float], set[int]]:
    """Maximum s-t flow by the Malhotra-Pramodh Kumar-Maheshwari algorithm.

    Vertices are 0 .. n-1 and edges (u, v, c) are directed with capacity c >= 0,
    an integer or a real number. Parallel and antiparallel edges are kept as
    separate edges, each with its own residual pair. Returns (value, flow, S):
    flow[i] is the flow on edges[i], and S is the source side of a minimum cut.
    Runs in O(n^3) time: at most n - 1 phases of O(n^2 + m) each.
    """
    if s == t:
        raise ValueError("s and t must differ")
    head: list[int] = []
    cap: list[float] = []
    adj: list[list[int]] = [[] for _ in range(n)]
    for u, v, c in edges:
        if c < 0:
            raise ValueError("capacities must be nonnegative")
        adj[u].append(len(head))
        head.append(v)
        cap.append(c)
        adj[v].append(len(head))
        head.append(u)
        cap.append(0)
    value = 0  # @line L1
    while True:
        level = bfs_levels(n, head, cap, adj, s)
        if level[t] < 0:  # @line L2
            break
        value += blocking_flow(n, head, cap, adj, level, s, t)
    flow = [c - cap[2 * i] for i, (_, _, c) in enumerate(edges)]
    return value, flow, {v for v in range(n) if level[v] >= 0}  # @line L10


def bfs_levels(n, head, cap, adj, s):
    """Distances from s in the residual graph, -1 for vertices s cannot reach."""
    level = [-1] * n
    level[s] = 0
    queue = deque([s])
    while queue:
        u = queue.popleft()
        for e in adj[u]:
            if cap[e] > 0 and level[head[e]] < 0:
                level[head[e]] = level[u] + 1
                queue.append(head[e])
    return level


def blocking_flow(n, head, cap, adj, level, s, t):
    """One phase: a blocking flow of the level graph, found by vertex potentials."""
    lt = level[t]
    alive = [level[v] >= 0 and (level[v] < lt or v == t) for v in range(n)]  # @line L3
    out_e = [[] for _ in range(n)]
    in_e = [[] for _ in range(n)]
    inp = [0] * n
    outp = [0] * n
    for u in range(n):
        if not alive[u]:
            continue
        for e in adj[u]:
            v = head[e]
            if cap[e] > 0 and alive[v] and level[v] == level[u] + 1:
                out_e[u].append(e)
                in_e[v].append(e)
                outp[u] += cap[e]
                inp[v] += cap[e]
    out_ptr = [0] * n
    in_ptr = [0] * n

    def dead_end(v):
        return (v != s and inp[v] == 0) or (v != t and outp[v] == 0)

    def delete(start):
        stack = [start]
        while stack:
            v = stack.pop()
            if not alive[v]:
                continue
            alive[v] = False
            for e in out_e[v]:
                w = head[e]
                if alive[w] and cap[e] > 0:
                    inp[w] -= cap[e]
                    if dead_end(w):
                        stack.append(w)
            for e in in_e[v]:
                w = head[e ^ 1]
                if alive[w] and cap[e] > 0:
                    outp[w] -= cap[e]
                    if dead_end(w):
                        stack.append(w)

    for v in range(n):
        if alive[v] and dead_end(v):
            delete(v)

    def potential(v):
        if v == s:
            return outp[v]
        if v == t:
            return inp[v]
        return min(inp[v], outp[v])

    total = 0
    while alive[s] and alive[t]:  # @line L4
        v = min((u for u in range(n) if alive[u]), key=potential)  # @line L5
        d = potential(v)  # @line L6
        touched = {v}
        send(v, d, level, lt, head, cap, out_e, out_ptr, alive, inp, outp, touched, forward=True)  # @line L7
        send(v, d, level, lt, head, cap, in_e, in_ptr, alive, inp, outp, touched, forward=False)  # @line L8
        total += d  # @line L9
        for u in touched:
            if alive[u] and (u == v or dead_end(u)):
                delete(u)
    return total


def send(v, d, level, lt, head, cap, lists, ptr, alive, inp, outp, touched, forward):
    """Move d units from v to t (forward) or from s to v (backward), level by level."""
    if d == 0:
        return
    stop = lt if forward else 0
    step = 1 if forward else -1
    buckets = {level[v]: [v]}
    excess = {v: d}
    lv = level[v]
    while lv != stop:
        for u in buckets.get(lv, []):
            x = excess[u]
            while x > 0:
                e = lists[u][ptr[u]]
                w = head[e] if forward else head[e ^ 1]
                if not alive[w] or cap[e] == 0:
                    ptr[u] += 1
                    continue
                y = min(x, cap[e])
                cap[e] -= y
                cap[e ^ 1] += y
                tail, hd = (u, w) if forward else (w, u)
                outp[tail] -= y
                inp[hd] -= y
                if w not in excess:
                    excess[w] = 0
                    buckets.setdefault(lv + step, []).append(w)
                excess[w] += y
                touched.add(w)
                x -= y
                if cap[e] == 0:
                    ptr[u] += 1
        lv += step
test_mpm_algorithm.py 86 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/mpm-algorithm/. The authors are listed in prov/QQPNK1.jsonl.

import itertools
import random
from fractions import Fraction

import pytest

from mpm_algorithm import mpm

# s, a, b, c, d, t = 0 .. 5
EXAMPLE = [(0, 1, 10), (0, 2, 10), (1, 2, 2), (1, 3, 4), (1, 4, 8), (2, 4, 9), (4, 3, 6), (3, 5, 10), (4, 5, 10)]


def cut_capacity(edges, side):
    return sum(c for u, v, c in edges if u in side and v not in side)


def brute_min_cut(n, edges, s, t):
    """Independent oracle: the least capacity over all s-t cuts."""
    others = [v for v in range(n) if v not in (s, t)]
    best = None
    for r in range(len(others) + 1):
        for extra in itertools.combinations(others, r):
            c = cut_capacity(edges, {s, *extra})
            best = c if best is None else min(best, c)
    return best


def check(n, edges, s, t, value, flow, side):
    net = [0] * n
    for (u, v, c), x in zip(edges, flow):
        assert 0 <= x <= c
        net[u] -= x
        net[v] += x
    for v in range(n):
        if v not in (s, t):
            assert net[v] == 0
    assert net[t] == value == -net[s]
    assert s in side and t not in side
    assert cut_capacity(edges, side) == value


def test_example() -> None:
    value, flow, side = mpm(6, EXAMPLE, 0, 5)
    assert value == 19
    assert side == {0, 2}
    check(6, EXAMPLE, 0, 5, value, flow, side)


def test_zero_capacity_and_no_path() -> None:
    edges = [(0, 1, 0), (1, 2, 5)]
    assert mpm(3, edges, 0, 2) == (0, [0, 0], {0})
    edges = [(0, 1, 4), (2, 3, 4)]
    value, flow, side = mpm(4, edges, 0, 3)
    assert (value, side) == (0, {0, 1})
    with pytest.raises(ValueError):
        mpm(2, [], 0, 0)
    with pytest.raises(ValueError):
        mpm(2, [(0, 1, -1)], 0, 1)


def test_antiparallel_parallel_and_loops() -> None:
    edges = [(0, 1, 3), (1, 0, 2), (0, 1, 1), (1, 2, 3), (2, 1, 7), (1, 1, 5)]
    value, flow, side = mpm(3, edges, 0, 2)
    assert value == 3
    check(3, edges, 0, 2, value, flow, side)


def test_real_capacities() -> None:
    edges = [(0, 1, Fraction(1, 3)), (0, 2, Fraction(1, 2)), (1, 2, Fraction(1, 7)), (1, 3, Fraction(1, 5)), (2, 3, Fraction(2, 3))]
    value, flow, side = mpm(4, edges, 0, 3)
    assert value == brute_min_cut(4, edges, 0, 3) == Fraction(5, 6)
    check(4, edges, 0, 3, value, flow, side)


def test_random_against_brute_force() -> None:
    rng = random.Random(31)
    for _ in range(800):
        n = rng.randint(2, 7)
        s, t = rng.sample(range(n), 2)
        edges = [(rng.randrange(n), rng.randrange(n), rng.randint(0, 6)) for _ in range(rng.randint(0, 3 * n))]
        value, flow, side = mpm(n, edges, s, t)
        assert value == brute_min_cut(n, edges, s, t)
        check(n, edges, s, t, value, flow, side)

What the next level needs

  • the tests did not run in this build

Read the full page about Malhotra-Pramodh Kumar-Maheshwari algorithm