Network Flow
Push water through a directed pipe network from source to sink: how much can you push? The max-flow min-cut theorem gives a startling answer — the maximum flow equals the minimum bottleneck cut. Edmonds-Karp guarantees polynomial time by always augmenting along the shortest available path. The same framework solves bipartite matching, vertex cover, and any problem that can be expressed as a flow.
Flow Networks
A flow network $G = (V, E)$ is a directed graph with a source $s$ and a sink $t$. Each edge $(u,v) \in E$ has a non-negative capacity $c(u,v)$; if $(u,v) \notin E$ then $c(u,v) = 0$. We assume no anti-parallel edges (no both $(u,v)$ and $(v,u)$) and no self-loops.
— Capacity constraint: $f(u,v) \leq c(u,v)$ for all $u,v$
— Flow conservation: $\sum_v f(u,v) = 0$ for all $u \neq s, t$ (flow in = flow out at every internal vertex, like Kirchhoff’s current law)
— Skew symmetry: $f(u,v) = -f(v,u)$ for all $u,v$
Value of flow: $|f| = f(s,V) = \sum_v f(s,v)$ = total flow out of source.
Key identity: $|f| = f(V,t)$ — flow out of source = flow into sink. Proof by flow conservation and skew symmetry.
Cuts and the Upper Bound
Lemma: $|f| = f(S,T)$ for any cut — the entire flow must cross every cut.
Corollary: $|f| \leq c(S,T)$ for any cut — flow is bounded by any bottleneck cut.
Residual Network and Augmenting Paths
To find a larger flow, look for paths in the residual network $G_f$: for each edge $(u,v)$, the residual capacity is $c_f(u,v) = c(u,v) - f(u,v)$. Include edge $(u,v)$ in $G_f$ iff $c_f(u,v) > 0$. Backward edges allow us to “undo” flow.
An augmenting path is any path $s \to t$ in $G_f$. We can push $c_f(p) = \min_{(u,v)\in p} c_f(u,v)$ more units of flow along $p$. After augmentation, $|f|$ increases by $c_f(p)$.
Max-Flow Min-Cut Theorem
- $|f| = c(S,T)$ for some cut $(S,T)$
- $f$ is a maximum flow
- $G_f$ contains no augmenting path
Proof sketch.
$(1) \Rightarrow (2)$: Since $|f| \leq c(S,T)$ for all cuts, equality with some cut implies no larger flow exists.
$(2) \Rightarrow (3)$: If an augmenting path existed, we could push more flow — contradicting maximality.
$(3) \Rightarrow (1)$: If no augmenting path exists, define $S = \{v : \exists \text{ path } s\to v \text{ in } G_f\}$, $T = V \setminus S$. Then $s \in S$, $t \in T$ (since $t$ is not reachable). For any $u \in S$, $v \in T$: $c_f(u,v) = 0$ (otherwise $v$ would be in $S$), so $f(u,v) = c(u,v)$. Thus $f(S,T) = c(S,T) = |f|$. $\square$
Ford-Fulkerson Algorithm
Repeatedly find an augmenting path in $G_f$ and augment along it until no augmenting path exists. With integer capacities, terminates with the maximum flow (each augmentation increases $|f|$ by at least 1). With irrational capacities, may not terminate.
Edmonds-Karp: $O(VE^2)$
Fix Ford-Fulkerson by always choosing the shortest augmenting path (BFS in $G_f$, each edge weight 1). This guarantees polynomial time.
Critical edge argument: Edge $(u,v)$ is critical on augmenting path $p$ if $c_f(u,v) = c_f(p)$ (it becomes the bottleneck, disappearing from $G_f$ after augmentation). For $(u,v)$ to be critical again, the reverse edge $(v,u)$ must appear on a later augmenting path, which requires $\delta(v)$ to have increased by at least 2 since the last time $(u,v)$ was critical. Since $\delta(v) \leq V-1$, each edge is critical at most $O(V)$ times. Total critical events: $O(VE)$. Each augmentation has at least one critical edge, so at most $O(VE)$ augmentations. Each BFS: $O(E)$. Total: $O(VE^2)$.
from collections import deque
def edmonds_karp(n, source, sink, capacity):
"""
Edmonds-Karp max flow (Ford-Fulkerson + BFS shortest augmenting path).
n: number of vertices (0-indexed).
capacity: 2D list, capacity[u][v] = c(u,v).
Returns (max_flow_value, flow_matrix).
Time: O(V * E^2).
"""
flow = [[0]*n for _ in range(n)] # current flow on each edge
def bfs_find_path():
"""Find shortest augmenting path s->t in residual graph via BFS."""
parent = [-1] * n
parent[source] = source
queue = deque([source])
while queue and parent[sink] == -1:
u = queue.popleft()
for v in range(n):
# Residual capacity: c_f(u,v) = capacity[u][v] - flow[u][v]
if parent[v] == -1 and capacity[u][v] - flow[u][v] > 0:
parent[v] = u
queue.append(v)
return parent if parent[sink] != -1 else None
max_flow = 0
while True:
parent = bfs_find_path()
if parent is None:
break # no augmenting path: done
# Find bottleneck capacity along the path
path_flow = float('inf')
v = sink
while v != source:
u = parent[v]
path_flow = min(path_flow, capacity[u][v] - flow[u][v])
v = u
# Augment flow along the path
v = sink
while v != source:
u = parent[v]
flow[u][v] += path_flow
flow[v][u] -= path_flow # update reverse edge (skew symmetry)
v = u
max_flow += path_flow
return max_flow, flow
def min_cut_edges(n, source, capacity, flow):
"""
Find the minimum cut edges from a max flow solution.
BFS on residual graph: S = vertices reachable from source.
Min-cut edges: (u,v) with u in S, v not in S, capacity[u][v] > 0.
"""
visited = [False] * n
queue = deque([source])
visited[source] = True
while queue:
u = queue.popleft()
for v in range(n):
if not visited[v] and capacity[u][v] - flow[u][v] > 0:
visited[v] = True
queue.append(v)
# Min-cut edges cross from S (visited) to T (not visited)
return [(u,v) for u in range(n) for v in range(n)
if visited[u] and not visited[v] and capacity[u][v] > 0]// Edmonds-Karp max flow — O(V * E^2)
#include <vector>
#include <queue>
#include <algorithm>
#include <climits>
using namespace std;
struct MaxFlow {
int n;
vector<vector<int>> cap, flow;
MaxFlow(int n) : n(n), cap(n, vector<int>(n,0)),
flow(n, vector<int>(n,0)) {}
void add_edge(int u, int v, int c) { cap[u][v] += c; }
// BFS: find shortest augmenting path, return parent array
vector<int> bfs(int s, int t) {
vector<int> par(n, -1); par[s] = s;
queue<int> q; q.push(s);
while (!q.empty() && par[t]==-1) {
int u = q.front(); q.pop();
for (int v=0;v<n;v++)
if (par[v]==-1 && cap[u][v]-flow[u][v]>0)
{ par[v]=u; q.push(v); }
}
return par;
}
int max_flow(int s, int t) {
int total = 0;
while (true) {
auto par = bfs(s, t);
if (par[t] == -1) break; // no augmenting path
// Bottleneck
int pf = INT_MAX;
for (int v=t; v!=s; v=par[v])
pf = min(pf, cap[par[v]][v] - flow[par[v]][v]);
// Augment
for (int v=t; v!=s; v=par[v]) {
flow[par[v]][v] += pf;
flow[v][par[v]] -= pf;
}
total += pf;
}
return total;
}
};Flow Integrality
This is crucial for applications: it means we can reduce integer optimisation problems to max flow and get integer solutions automatically.
Applications — Bipartite Matching
Given bipartite graph $G = (L \cup R, E)$, find the maximum matching (maximum set of edges with no shared endpoint).
1. Add source $s$ with edges $(s, l, 1)$ for all $l \in L$.
2. Add sink $t$ with edges $(r, t, 1)$ for all $r \in R$.
3. Direct each original edge $(l,r)$ with capacity $\infty$.
4. Max flow = size of maximum matching (by integrality: each $s\to t$ path is one matched edge).
Time: $O(VE)$ — $O(E)$ per augmentation, $O(V)$ augmentations (matching size $\leq \min(|L|,|R|)$).
Applications — Bipartite Vertex Cover (König’s Theorem)
In a bipartite graph, the size of the minimum vertex cover equals the size of the maximum matching (König’s theorem). Both equal the max flow in the constructed network.
def bipartite_max_matching(left_n, right_n, edges):
"""
Maximum bipartite matching via max flow (Edmonds-Karp).
left_n, right_n: sizes of left and right sets.
edges: list of (l, r) pairs (0-indexed within each side).
Returns (matching_size, matched_pairs).
Network: s -> L_i (cap 1), L_i -> R_j (cap inf), R_j -> t (cap 1).
Max flow = maximum matching size (by flow integrality).
O(V*E) time: O(V) augmentations (one per matched pair), O(E) per BFS.
"""
# Node numbering: 0=source, 1..left_n = left, left_n+1..left_n+right_n = right, last=sink
n = left_n + right_n + 2
s = 0
t = n - 1
INF = float('inf')
cap = [[0]*n for _ in range(n)]
# s -> each left vertex (cap 1)
for i in range(left_n):
cap[s][1 + i] = 1
# left -> right edges (cap inf — won't be bottleneck)
for l, r in edges:
cap[1 + l][left_n + 1 + r] = INF
# each right vertex -> t (cap 1)
for j in range(right_n):
cap[left_n + 1 + j][t] = 1
match_size, flow = edmonds_karp(n, s, t, cap)
# Extract matching from flow
matched = []
for l in range(left_n):
for r in range(right_n):
if flow[1+l][left_n+1+r] > 0:
matched.append((l, r))
return match_size, matched
def min_vertex_cover(left_n, right_n, edges):
"""
Minimum vertex cover in bipartite graph = max matching (König's theorem).
Returns (cover_size, left_in_cover, right_in_cover).
Uses the König construction: from max flow, find min cut,
then identify cover as (T ∩ L) ∪ (S ∩ R).
"""
n = left_n + right_n + 2
s, t = 0, n - 1
INF = float('inf')
cap = [[0]*n for _ in range(n)]
for i in range(left_n): cap[s][1+i] = 1
for l, r in edges: cap[1+l][left_n+1+r] = INF
for j in range(right_n): cap[left_n+1+j][t] = 1
_, flow = edmonds_karp(n, s, t, cap)
cut_edges = min_cut_edges(n, s, cap, flow)
# König's theorem: cover = vertices incident to min-cut edges
# S = reachable from s in residual, T = rest
from collections import deque
visited = [False]*n
q = deque([s]); visited[s] = True
while q:
u = q.popleft()
for v in range(n):
if not visited[v] and cap[u][v]-flow[u][v] > 0:
visited[v]=True; q.append(v)
# Left in cover: left vertices in T (not reachable)
left_cover = [i for i in range(left_n) if not visited[1+i]]
# Right in cover: right vertices in S (reachable)
right_cover = [j for j in range(right_n) if visited[left_n+1+j]]
return len(left_cover)+len(right_cover), left_cover, right_cover// Bipartite matching via max flow
// Returns size of maximum matching
int bipartite_matching(int L, int R,
const vector<pair<int,int>>& edges) {
// nodes: 0=source, 1..L=left, L+1..L+R=right, L+R+1=sink
int n = L + R + 2, s = 0, t = n-1;
MaxFlow mf(n);
for (int i=0;i<L;i++) mf.add_edge(s, 1+i, 1);
for (auto [l,r]:edges) mf.add_edge(1+l, L+1+r, 1e9);
for (int j=0;j<R;j++) mf.add_edge(L+1+j, t, 1);
return mf.max_flow(s, t);
}
// By König's theorem: min_vertex_cover = max_matching in bipartite graphsdef ford_fulkerson_dfs(n, source, sink, capacity):
"""
Ford-Fulkerson with DFS augmentation (can be very slow!).
Demonstrates why DFS path choice is dangerous.
Same network as Edmonds-Karp but uses DFS to find augmenting paths.
On the adversarial 4-node graph with capacities 10^9, this
takes 2*10^9 iterations where Edmonds-Karp takes 2.
"""
flow = [[0]*n for _ in range(n)]
def dfs_find_path():
visited = [False]*n
parent = [-1]*n
stack = [source]
visited[source] = True
parent[source] = source
while stack:
u = stack.pop()
if u == sink: return parent
for v in range(n):
if not visited[v] and capacity[u][v]-flow[u][v] > 0:
visited[v] = True
parent[v] = u
stack.append(v)
return None
total, iterations = 0, 0
while True:
parent = dfs_find_path()
if parent is None: break
# Bottleneck
pf, v = float('inf'), sink
while v != source:
u = parent[v]
pf = min(pf, capacity[u][v] - flow[u][v])
v = u
# Augment
v = sink
while v != source:
u = parent[v]
flow[u][v] += pf; flow[v][u] -= pf
v = u
total += pf; iterations += 1
return total, iterations
# Adversarial graph: 4 nodes, capacities that cause 2*10^9 iterations
# with DFS but only 2 with BFS (Edmonds-Karp)
cap_adversarial = [
[0, 10**9, 10**9, 0],
[0, 0, 1, 10**9],
[0, 1, 0, 10**9],
[0, 0, 0, 0],
]
# Edmonds-Karp handles this in 2 augmentations
ek_flow, _ = edmonds_karp(4, 0, 3, cap_adversarial)
print(f"Edmonds-Karp max flow: {ek_flow}") # 2*10^9, 2 iterations// Demonstrate the adversarial case for Ford-Fulkerson with DFS
// The 4-node graph with capacity 10^9 in the middle edge
// DFS: up to 2*10^9 iterations
// Edmonds-Karp (BFS): exactly 2 iterations
// With BFS, first augmentation: s->top->t (bottleneck 10^9)
// Second augmentation: s->bottom->t (bottleneck 10^9)
// Two augmentations total, flow = 2*10^9
// With DFS, alternating path through middle edge:
// Iteration 1: s->top->middle->t, cf=1
// Iteration 2: s->bottom->middle_rev->top->t... alternates
// Total: 2*10^9 iterations!
// Lesson: always use BFS (Edmonds-Karp) for shortest augmenting paths
// to guarantee O(VE^2) polynomial runtime.At expiry, TTT has multiple buy orders (strategies wanting to go long) and sell orders (strategies wanting to go short). Each buy order can be matched against certain sell orders based on price compatibility. Maximise the number of matched pairs.
# Buy orders: (order_id, max_price)
buys = [(0, 23500), (1, 24000), (2, 23000), (3, 24500)]
# Sell orders: (order_id, min_price)
sells = [(0, 23000), (1, 23500), (2, 24000)]
# Compatible pairs: buy_price >= sell_price
compatible = [
(b_id, s_id)
for b_id, b_px in buys
for s_id, s_px in sells
if b_px >= s_px
]
# compatible: (0,0),(0,1),(1,0),(1,1),(1,2),(2,0),(3,0),(3,1),(3,2)
matching_size, matched = bipartite_max_matching(
len(buys), len(sells), compatible
)
print(f"Maximum matched pairs: {matching_size}") # 3
for b, s in matched:
print(f" Buy {buys[b]} matched with Sell {sells[s]}")
In the max-flow min-cut theorem, if $f$ is a maximum flow and $(S^*, T^*)$ is the minimum cut, what is the exact relationship between $|f|$, $c(S^*, T^*)$, and $f(S^*, T^*)$?
We proved: (1) $|f| = f(S,T)$ for any cut (the flow across any cut equals the total flow value). (2) At the max flow, no augmenting paths exist in $G_f$. (3) Define $S^*$ = vertices reachable from $s$ in $G_f$. For any $u \in S^*$, $v \in T^* = V \setminus S^*$: $c_f(u,v) = 0$ (else $v$ would be in $S^*$), so $f(u,v) = c(u,v)$. Summing: $f(S^*,T^*) = c(S^*,T^*)$. Combined with (1): $|f| = f(S^*,T^*) = c(S^*,T^*)$.
Why does choosing BFS (shortest) augmenting paths guarantee $O(VE)$ augmentations, while DFS can require $O(|f^*|)$ augmentations?
The key is the monotonicity lemma: with BFS shortest paths, $\delta(v)$ never decreases after augmentations. When edge $(u,v)$ is critical (saturated), it disappears from $G_f$. For it to reappear and be critical again, the reverse edge $(v,u)$ must appear on some BFS path, which requires $\delta(u)$ to increase by $\geq 2$ (since $(v,u)$ on BFS path means $\delta(v) = \delta(u) - 1$, but after our augmentation $\delta(v) \geq$ previous $\delta(v)$, so new $\delta(u) \geq$ previous $\delta(v)+1 \geq$ previous $\delta(u)+2$). Since $\delta(u) \leq V-1$, each edge is critical $O(V)$ times: total $O(VE)$ augmentations. Each augmentation takes $O(E)$ for BFS: total $O(VE^2)$.
A flow network has all integer capacities. Ford-Fulkerson finds the max flow. Which statement is guaranteed by the integrality theorem?
The proof: Ford-Fulkerson starts from all-zero flow (integer). Each augmenting path has integer residual capacity (since all capacities and current flows are integers), so each augmentation pushes an integer amount. By induction, flows on all edges remain integers throughout. Therefore Ford-Fulkerson produces an integer-valued max flow. This is existence, not uniqueness: there could also be non-integer max flows (though they’d have the same value). Note: option D is wrong because LP solvers might find non-integer max flows for the same network; option C correctly says Ford-Fulkerson specifically finds an integer one.
In the bipartite vertex cover reduction to max flow, why must the min-cut have infinite-capacity edges removed — and what does this force about which vertices are in the cover?
The flow network has: $s \to L$ edges (capacity 1), $L \to R$ edges (capacity $\infty$), $R \to t$ edges (capacity 1). A finite-capacity cut $(S,T)$ can only include the unit-capacity edges. If any edge from $l \in S\cap L$ to $r \in T\cap R$ existed, it would cross the cut with infinite capacity — making $c(S,T) = \infty$. So for the cut to be finite, there must be no edges from $S\cap L$ to $T\cap R$ in the original bipartite graph. This means: every edge $(l,r) \in E$ has either $l \in T\cap L$ (left endpoint in $T$) or $r \in S\cap R$ (right endpoint in $S$). Setting the cover $Q = (T\cap L) \cup (S\cap R)$ covers all edges by this argument. And $|Q| = |T\cap L| + |S\cap R| = c(S,T)$. By max-flow min-cut, minimum such $c(S,T)$ equals the max flow = maximum matching.
Work through independently. Q1–3 direct application. Q4–7 synthesis. Q8–10 careful argument.