Most flow problems are modelling problems. The code below reuses Dinic from Max Flow (addEdge returns the edge id, flowOn(id), sourceSide(v)).

Choosing an algorithm

Problem shapeTool
maximum – flow, or min cut by LP dualityMax Flow (Dinic)
dense graph, unit capacities, or flow is the bottleneck over max-flow’s boundPush-Relabel
is edge in every / some minimum cut; min cut after a capacity changeMinimum Cut
cheapest cut over all vertex pairs, undirected graphGlobal Minimum Cut (Stoer–Wagner)
many pairwise min-cut queries on the same undirected graphGomory–Hu Tree
flow with per-unit costs, or lower bounds / supplies-and-demandsMin-Cost Flow
assign agents to (or ) slots minimising total cost, dense bipartiteAssignment Problem (Hungarian, ) — min-cost flow also works but is slower on dense inputs
maximum matching, bipartite, unweightedBipartite Matching (Kuhn’s / Hopcroft–Karp)
maximum matching, general graph, unweightedGeneral Matching (Blossom)
maximum-weight matching, general graphWeighted General Matching
independent set largest in two matroids simultaneouslyMatroid Intersection
two-sided preferences, no total cost to optimise, want stability not maximum weightStable Matching (Gale–Shapley) — this is not an optimization problem over a single objective, which is why it needs its own algorithm rather than falling out of weighted matching

Max-flow min-cut (proved there) is the reason the first several rows all reduce to the same machinery; stable matching is the odd one out precisely because “stable” isn’t a weight to maximize.

Construction patterns

RequirementConstruction
several sources / sinkssuper-source / super-sink with capacity = supply / demand
vertex capacity split with capacity
edge-disjoint / vertex-disjoint pathsunit capacities (split vertices for vertex-disjoint); Menger
bipartite matching / -matching (cap 1 or ), (cap 1), (cap 1 or )
each item assigned to one of its allowed slots, slot capacitiesthe same, with slot capacities on
time-expanded problems (people moving over steps)a copy of every vertex per time step, waiting edges
lower bounds demands transformation below
”choose ⟹ must choose ”, profits and costsmaximum weight closure (min cut)
binary labels with unary costs and pairwise “different labels” costsmin – cut (submodular energies)
many edges from a vertex to a range of verticessegment-tree helper nodes (Range Graph)
maximise ratio (edges / vertices)parametric cut + Dinkelbach (densest subgraph)
sports elimination (can team still win?)games as nodes with capacity = games left, teams capped at

Lower bounds and circulations

Edge with : send unconditionally (it adds to ‘s excess and removes it from ‘s), keep a residual edge of capacity . Connect positive-excess vertices from a super-source and negative-excess vertices to a super-sink ; a feasible circulation exists iff the max flow saturates all of ‘s edges. For an – flow with lower bounds add with infinite capacity first; for the maximum such flow, then remove that edge and augment ; for the minimum, augment and subtract.

struct LowerBoundFlow {
    int n; Dinic D; vector<ll> exc; vector<int> ids; vector<ll> lo;
    LowerBoundFlow(int n) : n(n), D(n + 2), exc(n) {}
    int addEdge(int u, int v, ll l, ll c) { exc[v] += l; exc[u] -= l; ids.push_back(D.addEdge(u, v, c - l)); lo.push_back(l); return sz(ids) - 1; }
    bool circulate() {                                       // feasible circulation?
        int S = n, T = n + 1; ll need = 0;
        for (int v = 0; v < n; v++) { if (exc[v] > 0) D.addEdge(S, v, exc[v]), need += exc[v]; else if (exc[v] < 0) D.addEdge(v, T, -exc[v]); }
        return D.maxflow(S, T) == need;
    }
    ll solve(int s, int t, bool mx) {                         // LLONG_MIN if infeasible; call once
        int id = D.addEdge(t, s, LLONG_MAX / 4, LLONG_MAX / 4); // both ways: the value may be negative
        if (!circulate()) return LLONG_MIN;
        ll base = D.flowOn(id); D.e[id].cap = D.e[id ^ 1].cap = 0;
        return mx ? base + D.maxflow(s, t) : base - D.maxflow(t, s);
    }
    ll maxFlow(int s, int t) { return solve(s, t, true); }
    ll minFlow(int s, int t) { return solve(s, t, false); }
    ll flow(int i) const { return lo[i] + D.flowOn(ids[i]); }
};

Classic uses: matrix rounding (round every entry up or down keeping row and column sums rounded: rows cells columns with ; always feasible), “each road used at least once” covers, staffing with minimum requirements, and minimum flow problems such as the minimum number of paths covering all edges of a DAG (lower bound 1 on every edge).

Where a lower bound shows upMeaning
”Every task must be done at least once” on the task edge
”Each machine runs at least hours”
Chinese postman / route covering every road per road
Scheduling with minimum staffing per shift required staff
Flow decomposition into exactly pathslower bound on the source edge

Combine with min-cost flow when the lower bounds carry a cost too: after the same transformation, run MCMF instead of max flow, and add back the mandatory at the end; negative edge costs then need Bellman–Ford potentials or an initial negative-cycle cancellation before the usual successive-shortest-paths loop.

Maximum weight closure (project selection)

Choose a set closed under implications maximising total weight: with capacity for , with for , with for each implication. Answer , and the source side is an optimal set.

pair<ll, vector<bool>> maxClosure(const vector<ll>& w, const vector<pair<int, int>>& imp) {   // u => v
    int n = sz(w), s = n, t = n + 1; Dinic D(n + 2); ll pos = 0, INF = 0;
    for (ll x : w) INF += llabs(x);
    INF++;
    for (int v = 0; v < n; v++) { if (w[v] > 0) D.addEdge(s, v, w[v]), pos += w[v]; else if (w[v] < 0) D.addEdge(v, t, -w[v]); }
    for (auto [u, v] : imp) D.addEdge(u, v, INF);
    ll cut = D.maxflow(s, t); vector<bool> ch(n);
    for (int v = 0; v < n; v++) ch[v] = D.sourceSide(v);
    return {pos - cut, ch};
}

Binary labelling (submodular): cost if gets label 0, if label 1, and if get different labels: edges (), (), (); label 0 = source side. Penalties for ” and ” only become a single directed edge . Non-submodular terms (rewarding different labels) can be handled on bipartite structures by flipping one side’s labels.

Densest subgraph

Maximise . For a guess , the network (), (), () has min cut , so a cut below certifies a denser set (the source side). Dinkelbach: start with and jump to the certified set until none exists; a few flows suffice, all in exact integers.

tuple<ll, ll, vector<int>> densestSubgraph(int n, const vector<pair<int, int>>& E) {   // density num/den and the set
    ll m = sz(E); if (!m) return {0, 1, {0}};
    vector<ll> deg(n); for (auto [u, v] : E) deg[u]++, deg[v]++;
    auto improve = [&](ll p, ll q, vector<int>& S) {
        int s = n, t = n + 1; Dinic D(n + 2);
        for (auto [u, v] : E) D.addEdge(u, v, q, q);
        for (int v = 0; v < n; v++) D.addEdge(s, v, q * m), D.addEdge(v, t, q * m + 2 * p - q * deg[v]);
        ll cut = D.maxflow(s, t); S.clear();
        for (int v = 0; v < n; v++) if (D.sourceSide(v)) S.push_back(v);
        return cut < q * m * n;
    };
    vector<int> best(n), S; iota(all(best), 0); ll num = m, den = n;
    while (improve(num, den, S)) {
        vector<char> in(n); for (int v : S) in[v] = 1;
        ll e = 0; for (auto [u, v] : E) e += in[u] && in[v];
        num = e, den = sz(S), best = S;
    }
    ll g = gcd(num, den); return {num / g, den / g, best};
}

Path covers and Dilworth

  • Vertex-disjoint paths covering a DAG: split each vertex into (left) and (right), edge becomes ; min paths max matching (each matched edge glues two path pieces).
  • Paths may share vertices (chains): first take the transitive closure, then the same. By Dilworth, the minimum number of chains = maximum antichain; a maximum antichain is read off König’s vertex cover (vertices with neither nor in the cover).
vector<vector<int>> minPathCover(int n, const vector<pair<int, int>>& E) {   // vertex-disjoint paths of a DAG
    Dinic D(2 * n + 2); int s = 2 * n, t = s + 1; vector<int> ids;
    for (int v = 0; v < n; v++) D.addEdge(s, v, 1), D.addEdge(n + v, t, 1);
    for (auto [u, v] : E) ids.push_back(D.addEdge(u, n + v, 1));
    D.maxflow(s, t);
    vector<int> nxt(n, -1), hasPrev(n);
    for (int i = 0; i < sz(E); i++) if (D.flowOn(ids[i])) nxt[E[i].first] = E[i].second, hasPrev[E[i].second] = 1;
    vector<vector<int>> paths;
    for (int v = 0; v < n; v++) if (!hasPrev[v]) { paths.push_back({}); for (int x = v; x != -1; x = nxt[x]) paths.back().push_back(x); }
    return paths;
}

Flow decomposition

Any flow decomposes into at most paths and cycles: repeatedly walk from along edges with positive flow; reaching closes a path, revisiting a vertex closes a cycle; subtract the bottleneck. Each extraction zeroes an edge. Used to output the actual routes (CSES Distinct Routes), and in proofs (integrality, path covers).

vector<pair<ll, vector<int>>> decompose(int n, int s, int t, vector<array<ll, 3>> fe) {   // {u, v, flow}; returns s-t paths (edge ids)
    vector<vector<int>> out(n); for (int i = 0; i < sz(fe); i++) out[fe[i][0]].push_back(i);
    vector<int> ptr(n); vector<pair<ll, vector<int>>> paths;
    auto nxt = [&](int u) { while (ptr[u] < sz(out[u]) && fe[out[u][ptr[u]]][2] <= 0) ptr[u]++; return ptr[u] < sz(out[u]) ? out[u][ptr[u]] : -1; };
    while (s != t && nxt(s) != -1) {
        vector<int> es, pos(n, -1), vs{s}; pos[s] = 0;
        while (vs.back() != t) {
            int e = nxt(vs.back()); if (e < 0) break;           // only happens if conservation fails
            int v = fe[e][1]; es.push_back(e);
            if (pos[v] >= 0) {                                  // cycle: cancel it and continue
                ll a = LLONG_MAX; for (int i = pos[v]; i < sz(es); i++) a = min(a, fe[es[i]][2]);
                for (int i = pos[v]; i < sz(es); i++) fe[es[i]][2] -= a;
                for (int i = pos[v] + 1; i < sz(vs); i++) pos[vs[i]] = -1;
                vs.resize(pos[v] + 1); es.resize(pos[v]); continue;
            }
            pos[v] = sz(vs); vs.push_back(v);
        }
        if (vs.back() != t) break;
        ll a = LLONG_MAX; for (int e : es) a = min(a, fe[e][2]);
        for (int e : es) fe[e][2] -= a;
        paths.push_back({a, es});
    }
    return paths;
}