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 shape | Tool |
|---|---|
| maximum – flow, or min cut by LP duality | Max Flow (Dinic) |
| dense graph, unit capacities, or flow is the bottleneck over max-flow’s bound | Push-Relabel |
| is edge in every / some minimum cut; min cut after a capacity change | Minimum Cut |
| cheapest cut over all vertex pairs, undirected graph | Global Minimum Cut (Stoer–Wagner) |
| many pairwise min-cut queries on the same undirected graph | Gomory–Hu Tree |
| flow with per-unit costs, or lower bounds / supplies-and-demands | Min-Cost Flow |
| assign agents to (or ) slots minimising total cost, dense bipartite | Assignment Problem (Hungarian, ) — min-cost flow also works but is slower on dense inputs |
| maximum matching, bipartite, unweighted | Bipartite Matching (Kuhn’s / Hopcroft–Karp) |
| maximum matching, general graph, unweighted | General Matching (Blossom) |
| maximum-weight matching, general graph | Weighted General Matching |
| independent set largest in two matroids simultaneously | Matroid Intersection |
| two-sided preferences, no total cost to optimise, want stability not maximum weight | Stable 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
| Requirement | Construction |
|---|---|
| several sources / sinks | super-source / super-sink with capacity = supply / demand |
| vertex capacity | split with capacity |
| edge-disjoint / vertex-disjoint paths | unit 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 capacities | the 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 costs | maximum weight closure (min cut) |
| binary labels with unary costs and pairwise “different labels” costs | min – cut (submodular energies) |
| many edges from a vertex to a range of vertices | segment-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 up | Meaning |
|---|---|
| ”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 paths | lower 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.
Proof: the lower-bound reduction
Write with . Conservation of at is with . A flow with these imbalances is exactly an – flow saturating the edges (capacity ) and (capacity ), since makes “saturate all edges” equivalent to “saturate all edges”. For the max – flow: the circulation found with the edge carries some – value ; deleting that edge, any further – augmentation in the residual graph keeps all lower bounds (residual capacities are relative to ) and the auxiliary are dead ends, so the maximum is plus the max flow in the residual network, by the standard augmenting-path optimality argument.
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};
}Proof: finite cuts are closures
A cut has finite capacity iff no implication edge goes from to , i.e. iff is closed. Its capacity is . Minimising the cut maximises the weight of the closure.
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};
}Proof: Goldberg's construction
For a source set (excluding ), the cut capacity is . Using , this simplifies to . So the min cut is below iff some nonempty has , i.e. density , and the minimising maximises . Dinkelbach’s iteration strictly increases the density each round and stops at the optimum; there are only finitely many density values.
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;
}Proof: paths matching
In any set of vertex-disjoint paths covering the DAG, the path edges form a matching between out-copies and in-copies (each vertex has at most one successor and one predecessor on its path), and the number of paths is minus the number of path edges. Conversely a matching defines successor/predecessor pointers that form vertex-disjoint paths (no cycles, since the graph is a DAG). So minimising paths is maximising the matching.
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;
}Proof: decomposition exists and terminates
Conservation guarantees that from any vertex reached by positive flow there is an outgoing edge with positive flow, so the walk either reaches or repeats a vertex. Subtracting the bottleneck along a path or cycle keeps conservation (each internal vertex loses the same amount in and out) and zeroes at least one edge, so there are at most extractions. The sum of the extracted path amounts equals the flow value since cycles carry no net – flow.