Back to Graph Theory Series

Karger's Min-Cut Algorithm

September 13, 2026 Wasil Zafar 22 min read

Pick a uniformly random edge, merge its endpoints, and keep parallel edges. One unlucky choice can destroy the optimum—but a short survival argument tells us exactly how repetition turns this tiny algorithm into a high-confidence global min-cut method.

Contents

  1. A Bit of History
  2. The Global-Cut Goal
  3. Algorithm Overview
  4. What Contraction Means
  5. Visual Walkthrough
  6. Survival Probability
  7. Confidence by Repetition
  8. Complexity Analysis
  9. Implementation
  10. Algorithm Comparison
  11. Applications
  12. Pitfalls
  13. Exercises

A Bit of History

David Karger developed the random-contraction approach in the early 1990s. Its lasting appeal is not that randomness magically makes every run correct; it is that a three-line idea admits a sharp, useful success bound. The algorithm became a canonical example of how independent repetition can convert a weak randomized primitive into a dependable method.

Karger and David Stein later improved the strategy by contracting only partway, branching recursively, and avoiding the cost of restarting every trial from the original graph. The basic algorithm remains the best place to learn the probability argument.

The Global-Cut Goal

For an undirected multigraph, a cut partitions the vertices into two non-empty sets. Its size is the number of edge copies crossing between the sets. The global minimum cut is the partition with the fewest crossing edges.

Parallel edges are not cosmetic: two copies represent two units of connectivity and count twice in the cut. For non-negative integer capacities, an edge of capacity $c$ can be viewed as $c$ parallel copies. More generally, weighted contraction samples an edge with probability proportional to its capacity; the implementations below focus on the explicit unweighted multigraph model.

uniform edge

Sample edge copies

Every current non-loop edge occurrence must have equal probability—not every vertex or distinct endpoint pair.

keep parallel

Multiplicity is capacity

Contraction can create several edges between two super-vertices. Retain every copy.

drop loops

Internal edges vanish

An edge whose endpoints have merged can cross no future cut, so it may be discarded.

Algorithm Overview

  1. Start with the current multigraph. Each vertex initially represents only itself.
  2. Choose one current edge uniformly at random. If parallel edges exist, each copy is a separate lottery ticket.
  3. Contract its endpoints. Unite the represented vertex groups, keep incident parallel edges, and remove newly created self-loops.
  4. Continue until two super-vertices remain. Their represented groups form a cut; the surviving edges between them are its size.
  5. Repeat independently. Return the smallest cut observed across all trials.

What Contraction Really Means

Contracting $u$ and $v$ declares that this run will never separate them. The new super-vertex represents the union of their original vertex sets. Any edge from either endpoint to a third vertex remains attached to the super-vertex; if both endpoints had such edges, parallel copies result. The edge $uv$ itself becomes a self-loop and disappears.

The Irreversible Choice

If a run contracts an edge belonging to a particular minimum cut, that cut can never be returned: its endpoints are now forced onto the same side. Success means avoiding every edge of at least one global minimum cut until only its two sides remain.

Visual Walkthrough

The multigraph below contains two dense triples joined by only two crimson edges. Each blue internal connection represents two parallel edge copies. The cluster boundary therefore has size $2$, while isolating any single vertex costs at least four edges.

A successful sequence of random contractions preserving a two-edge minimum cut Two densely connected three-vertex clusters are joined by two red edges. Internal blue edges are contracted until the two clusters become super-vertices, leaving the two red minimum-cut edges. A successful run avoids the crimson boundary 1 · Original multigraph2 · Contract internally3 · Two groups remain ABCDEF ×2×2×2×2×2×2 ABCEDF More safe blue contractions A,B,Cleft sideD,E,Fright side cut size = 2 Contract either red edge → this cut is lost Keep parallel edges: they are the surviving capacity between super-vertices. Different random choices produce different contraction histories and possibly different final cuts.
Luck has a precise meaning. This run is successful because it contracts only blue edges. Once each true side becomes one super-vertex, the two protected crimson edges are exactly the returned cut.

The Survival-Probability Argument

Fix one particular global minimum cut containing $k$ edge copies. Suppose the current contracted graph has $r$ super-vertices and the fixed cut has survived so far. Every vertex has degree at least $k$; otherwise isolating a lower-degree vertex would yield a cut smaller than the global minimum. By the handshake lemma, the graph therefore has at least $rk/2$ edges.

The probability that the next uniformly sampled edge belongs to our fixed cut is at most

$$\frac{k}{rk/2}=\frac{2}{r},$$

so the conditional probability of preserving it for the next step is at least $(r-2)/r$. Multiplying from $r=n$ down to $r=3$ telescopes:

$$\Pr[\text{one run preserves this cut}]\geq\prod_{r=3}^{n}\frac{r-2}{r}=\frac{2}{n(n-1)}=\binom{n}{2}^{-1}.$$

If the fixed cut survives every contraction, the final two super-vertices are precisely its two sides, so the run returns that minimum cut. Multiple distinct optimum cuts can only improve the chance of returning some optimum beyond this worst-case bound.

From One Lucky Run to High Confidence

Let $p=2/[n(n-1)]$ be the guaranteed per-trial success probability. With $T$ independent trials, the probability that all of them fail is at most

$$\Pr[\text{failure after }T]\leq(1-p)^T\leq e^{-pT}.$$

To target failure probability at most $\delta$, choose

$$T\geq\frac{\ln(1/\delta)}{p}=\frac{n(n-1)}{2}\ln\!\left(\frac1\delta\right).$$

The survival bound and confidence gained through repeated Karger trials A telescoping product gives one-run success at least two over n times n minus one. For twenty vertices, confidence is at least 0.53 percent after one trial, 41.00 percent after one hundred, 92.85 percent after five hundred, and 99.49 percent after one thousand. A tiny one-run chance compounds quickly At r verticesavoid cut ≥ (r−2)/rAcross n−2 contractions∏ (r−2)/rTelescoping resultp ≥ 2/[n(n−1)] Example: n = 20, so p ≥ 1/190 per independent trial 1 trial100 trials500 trials1,000 trials0.53%41.00%92.85%99.49% These are conservative lower bounds; a particular graph may be much easier.
Repetition is part of the algorithm. For $n=20$, a single run has only a $1/190$ worst-case guarantee. Independent restarts make the chance that every run is unlucky decay exponentially.

Confidence is a parameter, not a feeling. Choose an acceptable failure probability $\delta$, compute the corresponding trial count, and use independent random choices—or independent seeded streams—across trials.

Complexity Analysis

The transparent edge-list implementations below rebuild the list of currently crossing edges at each contraction. With $m$ original edge copies, one trial costs $O(nm)$ time and $O(n+m)$ auxiliary space. Across $T$ trials:

$$\text{Time: }O(Tnm) \qquad \text{Space: }O(n+m).$$

More specialized multigraph data structures can reduce the work per trial; the basic algorithm is often summarized as $O(n^2)$ per run under a suitable implementation. Achieving failure at most $1/n$ needs $T=O(n^2\log n)$ trials from the worst-case bound. Karger–Stein improves the overall expected running time to $O(n^2\log^3 n)$ by contracting only to roughly $n/\sqrt2$ vertices before branching recursively.

Implementation

The reference versions favor clarity: at each contraction they explicitly rebuild the list of edge copies whose endpoints are still in different components, then sample uniformly from that list. They return one observed partition as well as its cut size, detect disconnected graphs, and allow deterministic seeds for tests.

import math
import random

def karger_min_cut(
    n, edges, trials=None, failure_probability=1e-6, seed=None
):
    """Return (smallest_cut_seen, (side, complement)).

    Vertices are 0..n-1. Duplicate (u, v) entries are parallel edges.
    When trials is omitted, use the worst-case probability guarantee.
    """
    if n < 2:
        raise ValueError("a cut needs at least two vertices")

    edge_list = []
    for u, v in edges:
        if not (0 <= u < n and 0 <= v < n):
            raise ValueError("edge endpoint outside 0..n-1")
        if u != v:                 # input self-loops never cross a cut
            edge_list.append((u, v))

    def find(parent, x):
        while parent[x] != x:
            parent[x] = parent[parent[x]]
            x = parent[x]
        return x

    def union(parent, size, u, v):
        ru, rv = find(parent, u), find(parent, v)
        if ru == rv:
            return False
        if size[ru] < size[rv]:
            ru, rv = rv, ru
        parent[rv] = ru
        size[ru] += size[rv]
        return True

    # A disconnected graph has a legitimate global cut of size zero.
    initial_parent, initial_size = list(range(n)), [1] * n
    for u, v in edge_list:
        union(initial_parent, initial_size, u, v)
    initial_groups = {}
    for v in range(n):
        initial_groups.setdefault(find(initial_parent, v), set()).add(v)
    if len(initial_groups) > 1:
        side = next(iter(initial_groups.values()))
        return 0, (side, set(range(n)) - side)

    if trials is None:
        if not 0 < failure_probability < 1:
            raise ValueError("failure_probability must lie between 0 and 1")
        if n == 2:
            trials = 1
        else:
            p = 2 / (n * (n - 1))
            trials = math.ceil(math.log(failure_probability) / math.log1p(-p))
    if trials < 1:
        raise ValueError("trials must be positive")

    rng = random.Random(seed)
    best_cut = float("inf")
    best_side = None

    for _ in range(trials):
        parent, size = list(range(n)), [1] * n
        component_count = n

        while component_count > 2:
            crossing_edges = [
                (u, v) for u, v in edge_list
                if find(parent, u) != find(parent, v)
            ]
            u, v = rng.choice(crossing_edges)  # uniform over edge copies
            if union(parent, size, u, v):
                component_count -= 1

        groups = {}
        for v in range(n):
            groups.setdefault(find(parent, v), set()).add(v)
        side = next(iter(groups.values()))
        cut = sum(find(parent, u) != find(parent, v) for u, v in edge_list)

        if cut < best_cut:
            best_cut, best_side = cut, side

    return best_cut, (best_side, set(range(n)) - best_side)


edges = [
    (0, 1), (0, 2), (1, 2),
    (2, 3),
    (3, 4), (3, 5), (4, 5),
]
cut, partition = karger_min_cut(6, edges, seed=7)
print(cut)        # 1
print(partition)  # one side is {0, 1, 2}; set order may vary
#include <cstdint>
#include <iostream>
#include <limits>
#include <numeric>
#include <random>
#include <stdexcept>
#include <utility>
#include <vector>

class DSU {
public:
    explicit DSU(int n) : parent_(n), size_(n, 1) {
        std::iota(parent_.begin(), parent_.end(), 0);
    }
    int find(int x) {
        while (parent_[x] != x) {
            parent_[x] = parent_[parent_[x]];
            x = parent_[x];
        }
        return x;
    }
    bool unite(int a, int b) {
        a = find(a); b = find(b);
        if (a == b) return false;
        if (size_[a] < size_[b]) std::swap(a, b);
        parent_[b] = a;
        size_[a] += size_[b];
        return true;
    }
private:
    std::vector<int> parent_, size_;
};

struct CutResult {
    int size;
    std::vector<int> side;
};

CutResult kargerMinCut(int n, std::vector<std::pair<int, int>> input,
                       int trials, std::uint32_t seed) {
    if (n < 2 || trials < 1) throw std::invalid_argument("invalid n or trials");
    std::vector<std::pair<int, int>> edges;
    DSU connectivity(n);
    for (auto [u, v] : input) {
        if (u < 0 || u >= n || v < 0 || v >= n)
            throw std::invalid_argument("edge endpoint out of range");
        if (u != v) {
            edges.push_back({u, v});
            connectivity.unite(u, v);
        }
    }

    std::vector<int> firstComponent;
    int firstRoot = connectivity.find(0);
    for (int v = 0; v < n; ++v)
        if (connectivity.find(v) == firstRoot) firstComponent.push_back(v);
    if (static_cast<int>(firstComponent.size()) < n)
        return {0, firstComponent};

    std::mt19937 rng(seed);
    CutResult answer{std::numeric_limits<int>::max(), {}};
    for (int trial = 0; trial < trials; ++trial) {
        DSU dsu(n);
        int components = n;

        while (components > 2) {
            std::vector<int> candidates;
            for (int i = 0; i < static_cast<int>(edges.size()); ++i)
                if (dsu.find(edges[i].first) != dsu.find(edges[i].second))
                    candidates.push_back(i);

            std::uniform_int_distribution<int> pick(0, candidates.size() - 1);
            auto [u, v] = edges[candidates[pick(rng)]];
            if (dsu.unite(u, v)) --components;
        }

        int cutSize = 0;
        for (auto [u, v] : edges)
            cutSize += dsu.find(u) != dsu.find(v);

        if (cutSize < answer.size) {
            answer = {cutSize, {}};
            int root = dsu.find(0);
            for (int v = 0; v < n; ++v)
                if (dsu.find(v) == root) answer.side.push_back(v);
        }
    }
    return answer;
}

int main() {
    std::vector<std::pair<int, int>> edges = {
        {0,1}, {0,2}, {1,2}, {2,3}, {3,4}, {3,5}, {4,5}
    };
    CutResult cut = kargerMinCut(6, edges, 1000, 7);
    std::cout << "smallest cut seen = " << cut.size << '\n'; // 1
}
import java.util.*;

public class Karger {
    static final class DSU {
        final int[] parent, size;
        DSU(int n) {
            parent = new int[n]; size = new int[n];
            for (int i = 0; i < n; i++) { parent[i] = i; size[i] = 1; }
        }
        int find(int x) {
            while (parent[x] != x) {
                parent[x] = parent[parent[x]];
                x = parent[x];
            }
            return x;
        }
        boolean unite(int a, int b) {
            a = find(a); b = find(b);
            if (a == b) return false;
            if (size[a] < size[b]) { int t = a; a = b; b = t; }
            parent[b] = a; size[a] += size[b];
            return true;
        }
    }

    static final class CutResult {
        final int size;
        final List<Integer> side;
        CutResult(int size, List<Integer> side) {
            this.size = size; this.side = side;
        }
    }

    static CutResult minCut(int n, List<int[]> input, int trials, long seed) {
        if (n < 2 || trials < 1) throw new IllegalArgumentException("invalid n or trials");
        List<int[]> edges = new ArrayList<>();
        DSU connectivity = new DSU(n);
        for (int[] edge : input) {
            if (edge.length != 2 || edge[0] < 0 || edge[0] >= n ||
                    edge[1] < 0 || edge[1] >= n)
                throw new IllegalArgumentException("invalid edge");
            if (edge[0] != edge[1]) {
                edges.add(new int[]{edge[0], edge[1]});
                connectivity.unite(edge[0], edge[1]);
            }
        }

        List<Integer> firstComponent = new ArrayList<>();
        int firstRoot = connectivity.find(0);
        for (int v = 0; v < n; v++)
            if (connectivity.find(v) == firstRoot) firstComponent.add(v);
        if (firstComponent.size() < n) return new CutResult(0, firstComponent);

        SplittableRandom rng = new SplittableRandom(seed);
        CutResult answer = new CutResult(Integer.MAX_VALUE, List.of());
        for (int trial = 0; trial < trials; trial++) {
            DSU dsu = new DSU(n);
            int components = n;

            while (components > 2) {
                List<Integer> candidates = new ArrayList<>();
                for (int i = 0; i < edges.size(); i++) {
                    int[] e = edges.get(i);
                    if (dsu.find(e[0]) != dsu.find(e[1])) candidates.add(i);
                }
                int[] edge = edges.get(candidates.get(rng.nextInt(candidates.size())));
                if (dsu.unite(edge[0], edge[1])) components--;
            }

            int cutSize = 0;
            for (int[] e : edges)
                if (dsu.find(e[0]) != dsu.find(e[1])) cutSize++;

            if (cutSize < answer.size) {
                List<Integer> side = new ArrayList<>();
                int root = dsu.find(0);
                for (int v = 0; v < n; v++)
                    if (dsu.find(v) == root) side.add(v);
                answer = new CutResult(cutSize, side);
            }
        }
        return answer;
    }

    public static void main(String[] args) {
        List<int[]> edges = List.of(
            new int[]{0,1}, new int[]{0,2}, new int[]{1,2},
            new int[]{2,3}, new int[]{3,4}, new int[]{3,5}, new int[]{4,5}
        );
        CutResult cut = minCut(6, edges, 1000, 7);
        System.out.println(cut.size); // 1
        System.out.println(cut.side);
    }
}

Choosing a Global Min-Cut Algorithm

MethodGuaranteeMain ideaTrade-off
Basic KargerMonte Carlo; exact when successfulContract random edges to two groupsBeautifully simple, but needs many trials
Karger–SteinRandomized with amplificationContract partway, then branch recursivelyBetter asymptotics, more involved implementation
Stoer–WagnerDeterministic and exactMaximum-adjacency phases and safe contractionsPredictable result; designed for non-negative weighted undirected graphs
Repeated max flowDeterministic and exactFix one source and test all other sinksUseful when a mature max-flow routine already exists

Karger's strongest systems advantage is trial independence. Runs can be distributed across cores or machines, and only the best result needs to be reduced. That does not automatically make the basic algorithm fastest; graph size, representation, trial budget, and the availability of deterministic solvers all matter.

Where Random Contraction Helps

Probability intuition

A compact case study in conditional probability, telescoping products, and amplification.

Parallel trials

Independent runs require almost no coordination until their final cut values are compared.

Exact baselines

With a stated confidence budget, test graph heuristics against a simple randomized exact-cut candidate.

Network resilience

Estimate the weakest undirected separation, provided the probability of error is acceptable.

Cut discovery

Random contraction can expose sparse boundaries without selecting terminals in advance.

Algorithm design

The survival proof illustrates how preserving one hidden witness can certify a randomized algorithm.

Limitations and Common Pitfalls

MistakeWhy it mattersDefensive practice
Choosing a random vertex pairEndpoint pairs with many parallel edges become underweighted.Sample uniformly from current edge copies.
Collapsing parallel edgesChanges both cut sizes and future sampling probabilities.Preserve multiplicity; represent capacity by copies or weighted sampling.
Keeping self-loopsWastes samples on edges that can never cross a future cut.Discard or skip edges whose endpoints share a super-vertex.
Running only a handful of trialsThe worst-case one-run probability is only $\Theta(1/n^2)$.Derive $T$ from an explicit failure budget $\delta$.
Using correlated “independent” runsThe amplification formula may no longer apply.Use a sound generator and independent streams; record seeds for reproducibility.
Assuming a reported cut is certified optimalA Monte Carlo run can return a larger cut without warning.State confidence or verify with a deterministic algorithm when certainty is required.
Ignoring disconnected graphsContraction can stall before reaching two groups.Detect components first; disconnected graphs have cut value zero.
Expecting a balanced or semantic splitGlobal min-cut may isolate one low-degree vertex.Use balanced-cut, conductance, or domain-specific objectives when appropriate.

Monte Carlo Means One-Sided Error Here

Every returned partition is a real cut, so its size can never be smaller than the true optimum. The error is only that all trials may miss the optimum and return something too large. More independent trials can improve the answer; they can never invalidate a smaller cut already found.

Exercises

Make the Randomness Measurable

  1. Trace. On the illustrated multigraph, mark which first-edge choices immediately destroy the highlighted cut and compute that probability.
  2. Derive. Expand the telescoping product $(1-2/n)(1-2/(n-1))\cdots(1-2/3)$ and show that only $2/[n(n-1)]$ remains.
  3. Budget. For $n=50$ and target failure probability $10^{-6}$, compute the required trial count using the displayed formula.
  4. Test. Enumerate every non-trivial partition of small random multigraphs. Confirm that every returned cut is valid and that high-confidence runs match brute force.
  5. Weights. Adapt the Python sampler to edges $(u,v,c)$ by selecting with probability proportional to positive capacity $c$.
  6. Challenge. Implement Karger–Stein: contract independently to $\lceil n/\sqrt2\rceil+1$ vertices in two branches, recurse, and keep the smaller result.

Hint for 1: count edge copies, not drawn endpoint pairs; the six thick blue links each represent two lottery tickets.