Stoer-Wagner Global Minimum Cut — Junior Level¶
One-line summary: The Stoer-Wagner algorithm finds the global minimum cut of an undirected weighted graph — the cheapest way to split the vertices into two non-empty groups — without picking a fixed source and sink, by running
V-1"minimum cut phases" and merging two vertices after each, all inO(V³)time.
Table of Contents¶
- Introduction
- Prerequisites
- Glossary
- Core Concepts
- Big-O Summary
- Real-World Analogies
- Pros & Cons
- Step-by-Step Walkthrough
- Code Examples
- Coding Patterns
- Error Handling
- Performance Tips
- Best Practices
- Edge Cases & Pitfalls
- Common Mistakes
- Cheat Sheet
- Visual Animation
- Summary
- Further Reading
Introduction¶
Imagine a network of cities connected by roads, where each road has a "capacity" — say, how many cars per hour it can carry. Now ask: what is the cheapest set of roads I could cut to split the country into two pieces? Not "two specific pieces" — any split, as long as both halves are non-empty. The total capacity of the cut roads is what you want to minimize.
That is the global minimum cut problem, and the Stoer-Wagner algorithm (Mechthild Stoer and Frank Wagner, 1997) solves it for undirected, weighted graphs in a remarkably clean way.
Here is what makes it special. The classic textbook approach to "minimum cut" is the max-flow / min-cut theorem: pick a source s and a sink t, compute the maximum flow from s to t, and the value of that flow equals the minimum s-t cut. But the global min cut does not fix s and t — the two halves can be any partition. With max-flow you would have to try many s-t pairs (V-1 of them suffice, but it is still a lot of flow computations) and take the best.
Stoer-Wagner sidesteps all of that. It never computes a single flow. Instead it repeatedly runs a cheap procedure called a minimum cut phase. Each phase:
- Grows a set
Aone vertex at a time, always adding the vertex most tightly connected toA(this ordering is called maximum adjacency or a legal ordering). - The last two vertices added — call them
sandt— define the cut-of-the-phase: the cut that separatestfrom everyone else. Record its weight. - Merge
sandtinto a single combined vertex, then repeat the whole phase on the smaller graph.
After V-1 phases only one vertex remains, and the smallest cut-of-the-phase ever recorded is the global minimum cut. That is the entire algorithm. No flow, no augmenting paths, no source/sink choice — just "grow, record, merge," V-1 times.
The cost is O(V³) with a simple array implementation (or O(V·E + V² log V) with a heap), which is excellent for dense graphs of a few hundred vertices. It is the standard choice when you need the min cut of an entire undirected network.
Prerequisites¶
Before reading this file you should be comfortable with:
- Undirected weighted graphs — vertices, edges, and a non-negative weight on each edge (see sibling topic
01-representation). - Adjacency matrix — a
V × Vgrid wherew[i][j]is the weight of edge(i, j). Stoer-Wagner is easiest to write on a matrix. - Nested loops and simple array bookkeeping.
- The idea of a "cut" — splitting vertices into two non-empty groups and summing the weights of edges crossing between them.
- Big-O notation —
O(V³),O(V²).
Optional but helpful:
- Brief exposure to max-flow / min-cut (
19-max-flow-edmonds-karp-dinic) — you will appreciate why Stoer-Wagner avoids it. - Familiarity with Prim's MST — the maximum-adjacency ordering is structurally similar to Prim's "always add the closest vertex."
Glossary¶
| Term | Meaning |
|---|---|
| Cut | A partition of the vertices into two non-empty sets (S, V\S). |
| Cut weight | The sum of weights of all edges with one endpoint in S and the other in V\S. |
| Global minimum cut | The cut of smallest weight over all possible partitions (no fixed s, t). |
| s-t cut | A cut where a fixed vertex s is on one side and a fixed vertex t is on the other. |
| Minimum cut phase | One round: build a maximum-adjacency order, record the cut-of-the-phase, merge the last two vertices. |
| Maximum adjacency (legal) ordering | An order of vertices where each next vertex is the one most strongly connected (highest total edge weight) to the already-chosen set A. |
w(A, v) | The total weight of edges between vertex v and the current set A. The "connectedness" key. |
| Cut-of-the-phase | The cut separating the last added vertex t from all the rest, ({t}, V\{t}), in that phase. |
| Merge / contract | Combine two vertices s and t into one super-vertex; edge weights to common neighbors are added. |
| Super-vertex | A merged vertex that represents a group of original vertices fused together. |
| Dense graph | Many edges (close to V²); Stoer-Wagner's sweet spot. |
Core Concepts¶
1. What is a "cut," exactly?¶
A cut chops the vertex set V into two non-empty parts S and V\S. Its weight is the sum of the weights of every edge that has one endpoint in S and the other in V\S. The global minimum cut is the cut whose weight is smallest, over all possible ways to split the vertices.
Vertices {0,1,2,3}. Edges with weights:
0-1: 3 0-2: 1 1-2: 3 1-3: 1 2-3: 5
Cut S={3}, V\S={0,1,2}: crossing edges = (1-3)+(2-3) = 1+5 = 6
Cut S={0}, V\S={1,2,3}: crossing edges = (0-1)+(0-2) = 3+1 = 4
Cut S={0,2}, V\S={1,3}: crossing edges = (0-1)+(1-2)+(2-3) = 3+3+5 = 11
The smallest of these (and all others) is the global min cut.
2. Why not just use max-flow / min-cut?¶
The max-flow / min-cut theorem says: for a fixed s and t, the minimum s-t cut equals the maximum s-t flow. But the global min cut does not fix anything — the two sides can be any partition. To find it with max-flow you would fix one vertex (say vertex 0) as a permanent source and compute the min s-t cut to every other vertex as the sink — V-1 max-flow computations — and take the minimum. That works, but each flow computation is itself expensive, and you run V-1 of them.
Stoer-Wagner replaces all those flow computations with V-1 cheap phases, each just a maximum-adjacency scan. No flow is ever computed.
3. The minimum cut phase¶
A single phase works on the current (possibly already-merged) graph:
A = { arbitrary start vertex }
while A does not contain all vertices:
pick the vertex v NOT in A with the largest w(A, v) // most connected
add v to A
the last two vertices added are s (second-to-last) and t (last)
cut-of-the-phase = w(A, t) = total weight from t to everyone else
record this value, then MERGE s and t
The key quantity is w(A, v): how strongly vertex v is pulled toward the set A we have built so far. We always add the strongest-pulled vertex next. This is exactly like Prim's MST, except we maximize total connection weight instead of picking a single cheapest edge.
4. The cut-of-the-phase¶
When the phase ends, the last vertex added t is, by the ordering rule, the one most loosely connected — it was added last because everyone else was pulled in first. The cut-of-the-phase is simply ({t}, rest): isolate t. Its weight is w(A, t) at the moment t was added — the total weight of edges from t to all the vertices already in A.
The deep fact (proved in professional.md) is:
The cut-of-the-phase is always a minimum
s-tcut, wheresandtare the last two vertices added in that phase.
So each phase secretly computes a min s-t cut for one pair (s, t) — for free, without any flow.
5. Merge and repeat¶
After recording the cut-of-the-phase, we merge s and t into a single super-vertex. Merging means: any edge from s to a third vertex x and any edge from t to x are combined — their weights add. The merged vertex inherits the sum.
Why merge? Because we have already learned the best cut that separates s from t. From now on we only care about cuts that keep s and t together. Merging them enforces exactly that. After V-1 merges, the graph collapses to a single vertex, and we have considered enough (s, t) pairs to guarantee the global minimum is among the recorded cut-of-the-phase values.
6. The whole algorithm¶
best = +infinity
while graph has more than 1 vertex:
run a minimum cut phase → produces cut-of-the-phase weight and (s, t)
best = min(best, cut-of-the-phase weight)
merge s and t
return best
V-1 phases, each O(V²), gives O(V³).
Big-O Summary¶
| Aspect | Complexity | Notes |
|---|---|---|
| Time (array impl.) | O(V³) | V-1 phases × O(V²) per phase. |
| Time (heap impl.) | O(V·E + V² log V) | Use a priority queue for w(A, v); better on sparse graphs. |
| Space | O(V²) | The adjacency matrix (and merge bookkeeping). |
| Negative weights | Not supported | Weights must be non-negative for the proof to hold. |
| Directed graphs | Not supported | Stoer-Wagner is for undirected graphs only. |
| Output | min cut weight (and the partition, with extra bookkeeping) |
Compare: the max-flow approach to global min cut runs V-1 max-flow computations. On a dense graph that is far slower than one O(V³) Stoer-Wagner run.
Real-World Analogies¶
| Concept | Analogy |
|---|---|
| Global minimum cut | The cheapest set of cables to sever to split a power grid into two independent islands — and you do not care which two islands, only that the cut is cheapest. |
| Maximum-adjacency ordering | A party where you keep inviting whoever has the most friends already inside the room. The last person to get pulled in is the loner. |
| Cut-of-the-phase | Isolating that last "loner" guest — the cheapest single person to wall off from the room they just joined. |
Merging s and t | Two guests who turned out to be inseparable get a shared name tag and are treated as one person for the rest of the night. |
| Repeating phases | Keep fusing inseparable pairs until only one mega-group remains; the cheapest isolation you ever saw is the answer. |
| Network reliability | Finding the weakest link in a network — the smallest-capacity bottleneck that, if removed, disconnects it. |
Where the analogy breaks: in a real grid you usually do care which two islands you create (load balance, geography). Stoer-Wagner answers only the pure "cheapest split" question; constraints on which split need different tools.
Pros & Cons¶
| Pros | Cons |
|---|---|
Finds the global min cut — no need to guess s and t. | Only works on undirected graphs. |
| No max-flow required — simple "grow, record, merge." | Requires non-negative edge weights. |
O(V³) — great for dense graphs of a few hundred vertices. | O(V²) memory; large V is expensive. |
| Short, self-contained code (one matrix, two arrays). | Naive array version is slow on huge sparse graphs (use the heap variant). |
| Deterministic — always returns the exact optimum. | Recomputing after a graph change means re-running from scratch. |
| Same framework yields the actual partition with light bookkeeping. | Karger's randomized algorithm can be faster asymptotically on some inputs. |
When to use: undirected weighted graphs, you need the global min cut, the graph is dense or moderate in size (V up to ~a few hundred), weights are non-negative.
When NOT to use: directed graphs, you have negative weights, you only need a specific s-t cut (use max-flow directly), or V is huge and sparse (consider Karger-Stein or a heap-based variant).
Step-by-Step Walkthrough¶
Take this 4-vertex undirected weighted graph:
As an adjacency matrix w (symmetric, 0 on the diagonal):
We run phases until one vertex remains. best = ∞.
Phase 1 (vertices {0,1,2,3})¶
Start A = {0} (pick vertex 0 arbitrarily). Track w(A, v) for each v not in A:
Update keys (add edges from the newly added vertex 1):
w(A,2) = w[0][2]+w[1][2] = 1+3 = 4
w(A,3) = w[0][3]+w[1][3] = 0+1 = 1
→ max is vertex 2. Add 2. A={0,1,2}
Update keys (add edges from vertex 2):
The last two added are s=2 (second-to-last) and t=3 (last). Cut-of-the-phase = w(A,3) at the moment 3 was added = 6 (isolate vertex 3).
After merging 2 and 3, weights to the merged vertex add up:
New 3-vertex graph (vertices 0, 1, {2+3}):
Phase 2 (vertices {0, 1, 2+3})¶
Start A = {0}:
w(A,1)=3, w(A,{2+3})=1 → max is vertex 1. Add 1. A={0,1}
w(A,{2+3}) = 1+4 = 5 → add {2+3}. A={0,1,2+3}
Last two added: s=1, t={2+3}. Cut-of-the-phase = w(A,{2+3}) = 5 (isolate the super-vertex {2,3}, i.e. the partition {0,1} vs {2,3}).
After merging, the edge from 0 to the new super-vertex:
New 2-vertex graph (vertices 0, {1+2+3}):
Phase 3 (vertices {0, 1+2+3})¶
A={0}; add {1+2+3} (the only one left).
Cut-of-the-phase = 4 (partition {0} vs {1,2,3}).
best = min(5, 4) = 4
Only one vertex remains → stop.
Result¶
The recorded cut-of-the-phase weights were 6, 5, 4. The smallest is 4, achieved by the partition {0} | {1,2,3} — isolating vertex 0. The crossing edges are 0-1 (3) and 0-2 (1), total 4. That is the global minimum cut.
Code Examples¶
Example: Stoer-Wagner on an Adjacency Matrix¶
We implement the classic O(V³) array version. The matrix w[i][j] is modified in place as vertices merge; an active flag marks which rows are still "alive."
Go¶
package main
import "fmt"
// stoerWagner returns the weight of the global minimum cut of an
// undirected, non-negatively weighted graph given as an adjacency matrix.
// w is modified internally on a copy; the original is left intact.
func stoerWagner(n int, weight [][]int) int {
// work on a local copy so the caller's matrix is preserved
w := make([][]int, n)
for i := range w {
w[i] = append([]int(nil), weight[i]...)
}
merged := make([]bool, n) // a vertex that has been merged away
best := 1 << 60
// We need at least n-1 phases.
for phase := 0; phase < n-1; phase++ {
inA := make([]bool, n) // is vertex in the growing set A?
key := make([]int, n) // key[v] = w(A, v)
prev, last := -1, -1
// Add n-phase active vertices one by one.
active := 0
for i := 0; i < n; i++ {
if !merged[i] {
active++
}
}
for added := 0; added < active; added++ {
// pick the un-added active vertex with the largest key
sel := -1
for v := 0; v < n; v++ {
if !merged[v] && !inA[v] && (sel == -1 || key[v] > key[sel]) {
sel = v
}
}
inA[sel] = true
prev, last = last, sel
// update keys for the remaining vertices
for v := 0; v < n; v++ {
if !merged[v] && !inA[v] {
key[v] += w[sel][v]
}
}
}
// cut-of-the-phase = key[last] = w(A, last)
if key[last] < best {
best = key[last]
}
// merge `last` into `prev`
merged[last] = true
for v := 0; v < n; v++ {
if !merged[v] {
w[prev][v] += w[last][v]
w[v][prev] += w[v][last]
}
}
}
return best
}
func main() {
n := 4
w := [][]int{
{0, 3, 1, 0},
{3, 0, 3, 1},
{1, 3, 0, 5},
{0, 1, 5, 0},
}
fmt.Println("global min cut =", stoerWagner(n, w)) // 4
}
Java¶
import java.util.Arrays;
public class StoerWagner {
// Returns the weight of the global minimum cut. The matrix is copied.
static int minCut(int n, int[][] weight) {
int[][] w = new int[n][];
for (int i = 0; i < n; i++) w[i] = weight[i].clone();
boolean[] merged = new boolean[n];
int best = Integer.MAX_VALUE;
for (int phase = 0; phase < n - 1; phase++) {
boolean[] inA = new boolean[n];
int[] key = new int[n];
int prev = -1, last = -1;
int active = 0;
for (int i = 0; i < n; i++) if (!merged[i]) active++;
for (int added = 0; added < active; added++) {
int sel = -1;
for (int v = 0; v < n; v++)
if (!merged[v] && !inA[v] && (sel == -1 || key[v] > key[sel]))
sel = v;
inA[sel] = true;
prev = last;
last = sel;
for (int v = 0; v < n; v++)
if (!merged[v] && !inA[v]) key[v] += w[sel][v];
}
best = Math.min(best, key[last]); // cut-of-the-phase
merged[last] = true; // merge last into prev
for (int v = 0; v < n; v++) {
if (!merged[v]) {
w[prev][v] += w[last][v];
w[v][prev] += w[v][last];
}
}
}
return best;
}
public static void main(String[] args) {
int[][] w = {
{0, 3, 1, 0},
{3, 0, 3, 1},
{1, 3, 0, 5},
{0, 1, 5, 0}
};
System.out.println("global min cut = " + minCut(4, w)); // 4
}
}
Python¶
import copy
def stoer_wagner(n, weight):
"""Global minimum cut of an undirected, non-negatively weighted graph.
weight is an n x n adjacency matrix (symmetric, 0 on the diagonal).
Returns the weight of the global minimum cut. Time O(n^3).
"""
w = copy.deepcopy(weight)
merged = [False] * n
best = float("inf")
for _ in range(n - 1):
in_a = [False] * n
key = [0] * n
prev = last = -1
active = sum(1 for i in range(n) if not merged[i])
for _ in range(active):
sel = -1
for v in range(n):
if not merged[v] and not in_a[v] and (sel == -1 or key[v] > key[sel]):
sel = v
in_a[sel] = True
prev, last = last, sel
for v in range(n):
if not merged[v] and not in_a[v]:
key[v] += w[sel][v]
best = min(best, key[last]) # cut-of-the-phase
merged[last] = True # merge last into prev
for v in range(n):
if not merged[v]:
w[prev][v] += w[last][v]
w[v][prev] += w[v][last]
return best
if __name__ == "__main__":
w = [
[0, 3, 1, 0],
[3, 0, 3, 1],
[1, 3, 0, 5],
[0, 1, 5, 0],
]
print("global min cut =", stoer_wagner(4, w)) # 4
What it does: runs V-1 minimum cut phases on a copy of the matrix, records each cut-of-the-phase, merges the last two vertices, and returns the smallest recorded cut — 4 for our sample graph. Run: go run main.go | javac StoerWagner.java && java StoerWagner | python sw.py
Coding Patterns¶
Pattern 1: The Minimum Cut Phase¶
Intent: Grow A by maximum adjacency; the last vertex's key is the cut-of-the-phase.
def min_cut_phase(w, alive):
in_a = {v: False for v in alive}
key = {v: 0 for v in alive}
order = []
for _ in range(len(alive)):
sel = max((v for v in alive if not in_a[v]), key=lambda v: key[v])
in_a[sel] = True
order.append(sel)
for v in alive:
if not in_a[v]:
key[v] += w[sel][v]
s, t = order[-2], order[-1]
return key[t], s, t # cut-of-the-phase weight, and the pair to merge
Pattern 2: Merging Two Vertices¶
Intent: Fuse t into s; add edge weights to shared neighbors.
def merge(w, alive, s, t):
for v in alive:
if v != s and v != t:
w[s][v] += w[t][v]
w[v][s] += w[v][t]
alive.remove(t) # t no longer exists as its own vertex
Pattern 3: Tracking the Actual Partition¶
Intent: Remember which original vertices each super-vertex contains.
groups = {v: {v} for v in range(n)} # each vertex starts as its own group
# on merge(s, t): groups[s] |= groups[t]; del groups[t]
# when a new global best is found, the cut side is groups[t] (the isolated last vertex)
Error Handling¶
| Error | Cause | Fix |
|---|---|---|
| Wrong answer on a directed graph | Stoer-Wagner assumes undirected (symmetric matrix). | Only use on undirected graphs; symmetrize first or use a directed min-cut method. |
| Wrong answer with negative weights | The maximum-adjacency proof needs non-negative weights. | Reject or shift weights; the guarantee fails for negatives. |
IndexError / out-of-range | Matrix not sized to V, or vertex ids beyond V-1. | Size the matrix to exactly V × V and use 0-based ids. |
| Picking an already-merged vertex | Forgot the merged[] guard in the selection loop. | Always skip merged[v] and inA[v] when selecting. |
Returns +infinity | Graph has fewer than 2 vertices, or no phase ran. | Require V >= 2; a 1-vertex graph has no cut. |
| Mutated caller's matrix | Algorithm modifies w in place. | Work on a deep copy (as shown), or document the mutation. |
Performance Tips¶
- Use a single flat 1D array for the matrix (
w[i*n+j]) to improve cache locality on largeV. - Keep
w(A, v)keys in an array, not recomputed each step — update incrementally when a vertex joinsA. - Skip merged vertices early in the inner loops; a compact "alive list" beats scanning all
Vrows when many are merged. - For sparse graphs, use a max-priority-queue keyed on
w(A, v)(the heap variant):O(V·E + V² log V)instead ofO(V³). - Stop early if a cut-of-the-phase reaches a known lower bound (e.g. the minimum single-vertex degree is an upper bound, and a cut can never beat 0).
- The minimum weighted degree of any vertex is an immediate upper bound on the global min cut — initialize
bestwith it to prune.
Best Practices¶
- Always confirm the graph is undirected and weights are non-negative before running.
- Initialize
bestto the minimum weighted degree — a valid upper bound that can never be wrong. - Keep a
groups(orparent) structure if you need the actual partition, not just the cut weight. - Run on a copy of the matrix; the algorithm destroys it via merging.
- Write a brute-force
O(2^V)checker (try every subset) and cross-validate on tiny graphs (V <= 12). - Document that the array version is
O(V³); point to the heap variant for sparse inputs.
Edge Cases & Pitfalls¶
- Disconnected graph — the global min cut is
0(isolate any disconnected component). Stoer-Wagner returns0correctly becausew(A, t)will be0for an isolated piece. - Single edge bridge — the min cut equals that bridge's weight; isolating either endpoint side gives it.
- Self-loops — ignore them; an edge
v-vnever crosses a cut. Keep the diagonal at0. - Parallel edges — sum their weights into one matrix cell when building.
- Two-vertex graph — only one possible cut: the single edge between them.
- Zero-weight edges — allowed; they simply contribute
0to any cut crossing them.
Common Mistakes¶
- Using it on a directed graph — the symmetry assumption is essential; results are meaningless otherwise.
- Allowing negative weights — the maximum-adjacency ordering no longer guarantees the cut-of-the-phase is a min
s-tcut. - Merging the wrong pair — merge the last two added (
s, t), not arbitrary vertices. - Forgetting to update keys when a vertex joins
A— every remaining vertex's key gainsw[sel][v]. - Recording the wrong cut — the cut-of-the-phase is the key of the last vertex added, captured at the moment it joins.
- Stopping after one phase — you must run
V-1phases and take the minimum cut-of-the-phase.
Cheat Sheet¶
| Item | Value |
|---|---|
| Problem | Global minimum cut (undirected, weighted) |
| Time (array) | O(V³) |
| Time (heap) | O(V·E + V² log V) |
| Space | O(V²) |
| Negative weights | Not allowed |
| Directed graphs | Not supported |
| Phases | V-1 |
| Per phase | One maximum-adjacency scan, O(V²) |
| Answer | Smallest cut-of-the-phase over all phases |
Core loop:
best = min weighted degree
repeat V-1 times:
build maximum-adjacency order (each step: add the max-w(A,v) vertex)
s, t = last two added
best = min(best, w(A, t)) // cut-of-the-phase
merge s and t // add edge weights to shared neighbors
return best
Phase invariant:
Visual Animation¶
See
animation.htmlfor an interactive visual animation of the Stoer-Wagner minimum cut phase.The animation demonstrates: - The maximum-adjacency ordering building up vertex by vertex (
Agrows) - Thew(A, v)key of each candidate vertex, updating asAgrows - The cut-of-the-phase highlighted between the last two added verticessandt- The merge ofsandtinto a super-vertex, with edge weights adding - The running global minimum tracked across phases - Step / Run / Reset controls, a Big-O table, and an operation log
Summary¶
Stoer-Wagner finds the global minimum cut of an undirected weighted graph without ever computing a max-flow. It runs V-1 minimum cut phases; each phase grows a set A by always adding the most-connected vertex (maximum adjacency), records the cut-of-the-phase that isolates the last vertex added, and then merges the last two vertices. The smallest cut-of-the-phase across all phases is the answer. The array version is O(V³) time and O(V²) space — ideal for dense graphs of moderate size. Remember: undirected only, non-negative weights only, and always run all V-1 phases.
Next step:¶
Continue to middle.md to learn why the cut-of-the-phase is correct, when Stoer-Wagner beats running max-flow V-1 times, and how the heap variant changes the complexity.
Further Reading¶
- Mechthild Stoer and Frank Wagner, A Simple Min-Cut Algorithm, JACM 44(4):585–591, 1997 — the original paper.
- Introduction to Algorithms (CLRS) — chapters on flow networks and cuts for the max-flow / min-cut background.
- cp-algorithms.com — "Minimum cut: Stoer-Wagner algorithm."
- Sibling topics:
19-max-flow-edmonds-karp-dinic(s-t cuts via flow),01-representation,21-karger-min-cut(randomized global min cut). - David Karger, Global Min-cuts in RNC — the randomized contraction alternative.