Cairn
algorithms · network flow · O(V³), deterministic

back to Network Flow

Stoer-Wagner Algorithm

Karger's Algorithm answers the global minimum cut question — the fewest edges whose removal disconnects a graph, over every possible way to split it, no fixed source or sink — by repeatedly contracting a uniformly random edge and hoping the true minimum survives. It usually needs many repeats to reach real confidence, because any single run can miss the answer. The Stoer-Wagner algorithm, published by Mechthild Stoer and Frank Wagner in 1997, answers the exact same question with no randomness at all: every run finds the true minimum, every time. It's also a different deterministic approach from the one Karger's own page names in passing — fixing a source and running n − 1 max-flow computations against every other node as sink — Stoer-Wagner needs no max-flow subroutine, no residual graph, no augmenting paths whatsoever, just a single repeated greedy scan.

The core step, called a minimum cut phase, is a maximum adjacency search: start from any vertex and grow a set A one vertex at a time, always adding whichever vertex outside A currently has the largest total edge weight into A — the same "grow a frontier, always take the most tightly connected candidate" shape Prim's Algorithm uses for minimum spanning trees, just measuring total connection strength instead of a single cheapest edge. Once every vertex has joined, the phase is done. What falls out of that one scan is a specific, provably useful cut, described below.

Try it

The same six nodes and seven edges as Karger's Algorithm's own demo — a triangle A-B-C, a triangle D-E-F, and a single bridge edge C-D — so the two pages are directly comparable on the identical graph. The true global minimum cut is that one bridge edge, size 1. Press Step or Run: each step either adds a node to the current phase's growing set A (highlighted, with its weight-to-A shown in the table below the graph) or merges the phase's last two vertices into one supernode (recolored, same convention as Karger's page) once a phase completes. Unlike Karger's demo, this one is fully deterministic — press Reset and Run again and every step repeats identically, no randomness anywhere.

phase — / 5 · supernodes remaining: 6 · best cut so far:
Press Step or Run.

Why merging never loses the true minimum

Call the last two vertices a phase's maximum adjacency search adds s (second-to-last) and t (last). The key lemma the whole algorithm rests on: the cut that isolates {t} from everything else — exactly the weight-to-A value t had when it joined, what this page calls the phase's cut-of-the-phase — is the true minimum s-t cut in the current graph, not merely some upper bound on it. That guarantees merging s and t together can never make the true global minimum cut unreachable: either the global minimum happens to separate s from t, in which case this phase's cut-of-the-phase already equals it exactly, or it doesn't — s and t sit on the same side of it — in which case merging them changes that cut's weight not at all, and a later phase, working on the smaller merged graph, is still free to find it. Running n − 1 phases, one vertex removed each time, means the smallest cut-of-the-phase recorded across all of them is guaranteed to be the true global minimum — proved, not just observed to work on friendly examples. On the demo's graph, phase 3's cut-of-the-phase (isolating the merged D+E+F supernode, weight 1) is that minimum; every other phase's cut-of-the-phase comes in at 2.

Reference implementation

Matches the demo above one for one — same maximum-adjacency order, same merge bookkeeping, just without the per-step snapshots the demo keeps for stepping through:

function stoerWagnerMinCut(n, W) {
  // W: n x n symmetric weight matrix, W[i][j] === 0 means no edge
  const weight = W.map(row => row.slice());
  let active = Array.from({ length: n }, (_, i) => i);
  let minCut = Infinity;

  while (active.length > 1) {
    const inA = new Set();
    const key = {};
    active.forEach(v => { key[v] = 0; });

    const start = active[0];
    inA.add(start);
    active.forEach(v => { if (v !== start) key[v] = weight[start][v]; });

    let s = start, t = start, cutOfPhase = -Infinity;
    while (inA.size < active.length) {
      let next = -1, best = -Infinity;
      active.forEach(v => { if (!inA.has(v) && key[v] > best) { best = key[v]; next = v; } });
      s = t; t = next; cutOfPhase = best;
      inA.add(next);
      active.forEach(v => { if (!inA.has(v)) key[v] += weight[next][v]; });
    }

    minCut = Math.min(minCut, cutOfPhase);

    // merge t into s: every other active vertex's weight to s absorbs its weight to t
    active.forEach(v => {
      if (v === s || v === t) return;
      weight[s][v] += weight[t][v];
      weight[v][s] += weight[v][t];
    });
    active = active.filter(v => v !== t);
  }

  return minCut;
}

Every original edge contributes weight 1 to W for an unweighted graph like the demo's; a weighted graph works identically, since nothing about the algorithm assumes unit weights anywhere.

Pitfalls

The merge step must accumulate, not overwrite. After a phase, vertex s can already share an edge with some other vertex v, and t can too — the merged supernode's true connection to v is the sum of both, not just whichever one gets written last. Replacing weight[s][v] += weight[t][v] with a plain weight[s][v] = weight[t][v] looks harmless (it still runs, still returns some number) but silently drops real connectivity every time a vertex happens to share an edge with both endpoints of a merge — which is common, not rare. Checked directly: on this page's own default graph, the buggy version reports a global minimum cut of 0 instead of the true 1, wrongly claiming the graph is already disconnected — toggle the checkbox above and press Reset, Run to watch phase 3 land on 0 instead of 1 in real time, on the exact same seven edges the correct version handles fine. Measured more broadly: 1,024/1,024 possible unweighted graphs on 5 labeled vertices tested exhaustively against a brute-force minimum cut, the overwrite bug disagreed on 657 of them (64.2%); across 3,000 randomized weighted graphs (4 to 9 vertices), it disagreed on 77.8%.

Stopping one phase early looks fine right up until it isn't. The loop above must keep merging until exactly one supernode remains (n − 1 total phases) — stopping once two supernodes are left, on the assumption that "the last phase can't possibly find anything new," skips checking that very last phase's own cut-of-the-phase value. On this page's own demo graph, that shortcut happens to cause no visible harm: the true minimum (1) is already found back in phase 3, well before the final phase would have run, so an early-stopping version still reports the right answer here — which is exactly what makes this version of the bug dangerous to trust from testing against one friendly graph. Measured directly against a brute-force check: exhaustively over the same 1,024 five-vertex graphs, stopping one phase early disagreed with the true minimum on 90 of them (8.8%); over 3,000 randomized weighted graphs (4 to 9 vertices), 11.4%. Both numbers are real measured disagreement rates, not a hypothetical worst case — and both bugs were caught by comparing against brute force, not by reasoning about the code.

Complexity

Time: O(V³) for the array-based version shown above and used by the demo — V − 1 phases, each phase's maximum adjacency search doing up to V vertex additions, each addition scanning all remaining active vertices in O(V) to find the next maximum. The textbook bound with a Fibonacci heap keyed on live connectivity, O(VE + V² log V), needs a priority queue supporting efficient key increases — the same array-vs-heap tradeoff Dijkstra's Algorithm and Prim's Algorithm both make for the code shown on their own pages, not a claim that the simple version is what production code should use on a large graph. Space: O(V²) for the dense weight matrix — every phase can, in principle, need the weight between any two remaining supernodes, which an adjacency-list representation can't answer in O(1) without extra bookkeeping.

Compared to Karger's Algorithm's naive O(n⁴ log n) for high-probability success, Stoer-Wagner's O(V³) is asymptotically better and never needs repeating — but Karger's per-run cost is far cheaper, which is exactly why the randomized approach can still win in practice on graphs dense enough that its many-repeats total stays below a single deterministic pass. See Choosing a Network Flow Algorithm for how this entry compares against all ten of the site's other Network Flow pages side by side.