The cheapest way to split an undirected weighted graph into two non-empty parts, over all pairs. With unit weights it is the edge connectivity . Fixing and running max flows works but is slow; write Stoer–Wagner.

AlgorithmTimeNotes
max flows from a fixed maxflowalso the only simple option for directed graphs: , flows
Stoer–Wagner (Nagamochi–Ibaraki) matrix, heapdeterministic, write this
Karger contraction for high probabilitythe counting argument is the point
Karger–Steinrandomized recursion
Karger (tree packing), Kawarabayashi–Thorupnear-linearnot for contests

Stoer–Wagner

A maximum adjacency ordering adds, each step, the vertex with the largest total weight to the already-added set. The last vertex alone against everything else is a minimum – cut for the last two vertices ; record it, merge and , repeat.

pair<ll, vector<int>> stoerWagner(vector<vector<ll>> w) {   // symmetric matrix; returns {cut, one side}
    int n = sz(w); ll best = LLONG_MAX; vector<int> cut;
    vector<vector<int>> group(n); for (int i = 0; i < n; i++) group[i] = {i};
    for (int ph = 1; ph < n; ph++) {
        vector<ll> key = w[0]; vector<char> added(n); int s = 0, t = 0;
        for (int it = 0; it < n - ph; it++) {                // vertex 0 always survives and starts the ordering
            key[t] = LLONG_MIN; s = t;
            t = max_element(all(key)) - key.begin();
            for (int i = 0; i < n; i++) if (key[i] != LLONG_MIN) key[i] += w[t][i];
        }
        if (key[t] - w[t][t] < best) best = key[t] - w[t][t], cut = group[t];
        group[s].insert(group[s].end(), all(group[t]));
        for (int i = 0; i < n; i++) w[s][i] += w[t][i], w[i][s] = w[s][i];
        w[0][t] = LLONG_MIN;                                  // t is gone: never selected again
    }
    return {best, cut};
}

The merged vertex is removed by setting w[0][t] to , which seeds key[t] with in later phases; adding finite weights never brings it back above real keys. With a heap and adjacency lists the same is .

Karger contraction and counting minimum cuts

Contract uniformly random edges (probability proportional to weight) until two super-vertices remain; the edges between them form a cut. A fixed minimum cut survives with probability at least , so runs succeed w.h.p.; a run is Kruskal on a random edge order.

Corollary: a graph has at most global minimum cuts (tight for a cycle), and at most cuts of weight .

Karger–Stein

Contracting from to keeps a fixed minimum cut with probability : the danger is only at the end, so contract to twice independently and recurse on both. , success probability ; repeat times.

mt19937_64 krng(1234567);
ll kargerStein(vector<vector<ll>> w) {                      // one run; repeat and take the minimum
    int n = sz(w);
    if (n <= 6) {
        ll best = LLONG_MAX;
        for (int mask = 1; mask < (1 << n) - 1; mask++) {
            ll c = 0; for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) if ((mask >> i & 1) && !(mask >> j & 1)) c += w[i][j];
            best = min(best, c);
        }
        return best;
    }
    auto contract = [&](vector<vector<ll>> a, int t) -> vector<vector<ll>> {
        int m = sz(a); vector<ll> deg(m); vector<int> alive(m); iota(all(alive), 0);
        for (int i = 0; i < m; i++) deg[i] = accumulate(all(a[i]), 0LL);
        while (sz(alive) > t) {
            ll tot = 0; for (int v : alive) tot += deg[v];
            if (!tot) return {};                              // disconnected: the answer is 0
            ll r = uniform_int_distribution<ll>(0, tot - 1)(krng); int u = 0, v = 0;
            for (int x : alive) { if (r < deg[x]) { u = x; break; } r -= deg[x]; }
            r = uniform_int_distribution<ll>(0, deg[u] - 1)(krng);
            for (int x : alive) { if (r < a[u][x]) { v = x; break; } r -= a[u][x]; }
            deg[u] += deg[v] - 2 * a[u][v];
            for (int x : alive) a[u][x] += a[v][x], a[x][u] = a[u][x];
            a[u][u] = 0; alive.erase(find(all(alive), v));
        }
        vector<vector<ll>> b(t, vector<ll>(t));
        for (int i = 0; i < t; i++) for (int j = 0; j < t; j++) b[i][j] = a[alive[i]][alive[j]];
        return b;
    };
    int t = (int)ceil(1 + n / sqrt(2.0)); ll best = LLONG_MAX;
    for (int rep = 0; rep < 2; rep++) {
        auto b = contract(w, t);
        if (b.empty()) return 0;
        best = min(best, kargerStein(b));
    }
    return best;
}

Sparse certificates (Nagamochi–Ibaraki)

To decide cuts smaller than , replace by the union of forests where is a maximal spanning forest of . It has at most edges and preserves every cut of size exactly. Greedy one pass: put each edge into the first forest whose DSU does not yet connect its endpoints; connectivity is monotone in , so binary search the forest. Use before -edge-connectivity checks, small max flows, or Stoer–Wagner on dense multigraphs. (Nagamochi–Ibaraki obtain the same forests in from one MA ordering.)

vector<int> sparseCertificate(int n, const vector<pair<int, int>>& E, int k) {   // ids of the kept edges
    vector<DSU> F(k, DSU(n)); vector<int> keep;
    for (int i = 0; i < sz(E); i++) {
        auto [u, v] = E[i]; int lo = 0, hi = k;              // first forest not connecting u and v
        while (lo < hi) { int mid = (lo + hi) / 2; if (F[mid].find(u) == F[mid].find(v)) lo = mid + 1; else hi = mid; }
        if (lo < k) F[lo].unite(u, v), keep.push_back(i);
    }
    return keep;
}

Applications

Beyond raw edge connectivity: network reliability (the min cut is the bottleneck against random failure), clustering and community detection, image segmentation (min-cut partitioning of a pixel similarity graph), and circuit partitioning in VLSI design (minimising cross-partition wiring). Certificates (above) are maintained incrementally inside dynamic connectivity structures, and the same “sparsify before you compute” philosophy produced cut sparsifiers (Benczúr-Karger) and spectral sparsifiers (Spielman-Teng), preserving all cuts approximately within using edges.