/
Progress
9 / 13
← All Chapters
Advanced Chapter 09 of 13 · Algorithm Design and Analysis

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.

Net flow $f: V \times V \to \mathbb{R}$ satisfying three properties:
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

A cut $(S,T)$ is a partition of $V$ with $s \in S$, $t \in T$. Flow across the cut: $f(S,T) = \sum_{u\in S}\sum_{v\in T} f(u,v)$. Capacity of cut: $c(S,T) = \sum_{u\in S}\sum_{v\in T} c(u,v)$.

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

Theorem. The following three conditions are equivalent:
  1. $|f| = c(S,T)$ for some cut $(S,T)$
  2. $f$ is a maximum flow
  3. $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.

Ford-Fulkerson can be slow. With 4 vertices and capacities $10^9$, choosing the middle edge as part of every augmenting path takes $2 \times 10^9$ iterations, each augmenting by 1. The algorithm runs in $O(E \cdot |f^*|)$ where $|f^*|$ is the max flow value — pseudo-polynomial, not polynomial in the input size.

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.

Key lemma (Monotonicity): Let $\delta(v) = \delta_f(s,v)$ be the BFS distance from $s$ to $v$ in $G_f$. After any augmentation, $\delta(v)$ can only increase (never decrease) for all $v$.

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)$.
Edmonds-Karp Maximum Flow · O(VE²)
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;
    }
};
↑ Run C++ on Compiler Explorer

Flow Integrality

Integrality theorem. If all capacities are integers, then Ford-Fulkerson (including Edmonds-Karp) produces an integer-valued maximum flow. Each augmentation increases the flow by an integer amount; since we start from 0, the flow is always integral.

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).

Reduction to max flow:
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.

Minimum vertex cover via max flow. Build the same flow network. For any vertex cover $Q = Q_L \cup Q_R$: define cut $S = \{s\} \cup Q_R \cup (L \setminus Q_L)$, $T = \{t\} \cup Q_L \cup (R \setminus Q_R)$. Cut capacity $= |Q_L| + |Q_R| = |Q|$. Conversely, any finite-capacity cut $(S,T)$ yields vertex cover $Q = (S\cap R) \cup (T\cap L)$ of size $c(S,T)$. Therefore: min vertex cover $=$ min cut $=$ max flow. $\square$
Bipartite matching + vertex cover via max flow · O(VE)
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 graphs
↑ Run C++ on Compiler Explorer
Ford-Fulkerson vs Edmonds-Karp: the slow-path demonstration
def 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.
↑ Run C++ on Compiler Explorer
Worked Example · Order Matching as Bipartite Flow 🇮🇳 TTT Trading

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.

Model as bipartite matching via max flow
# 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]}")
Result: 3 pairs matched (maximum possible — limited by 3 sell orders). The max-flow min-cut theorem tells us the bottleneck: it’s the sell side. Maximum matching = minimum vertex cover by König’s theorem: the minimum set of orders that “covers” all compatible pairs is the entire sell side (3 orders).
Network flow as a modelling language is extremely powerful. In quant finance: baseball elimination (can a team still win the league?) reduces to max flow. Job shop scheduling with resource constraints is a flow problem. At TTT, position limits across strategies can be modelled as a flow problem: source = capital, intermediate nodes = strategies, sink = market positions, capacities = position limits. Max flow tells you the maximum deployed capital respecting all constraints simultaneously. The integrality theorem ensures that if position sizes are integer lots, the optimal solution is automatically integer-valued.

Practice Problems
4 questions · Chapter 09
prog / daa / ch09 / q01 ★ Max-Flow Min-Cut

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^*)$?

A
$|f| \leq c(S^*,T^*)$ and $|f| = f(S^*,T^*)$ but they may differ
B
$|f| = f(S^*,T^*) = c(S^*,T^*)$ — the flow value equals the flow across the min-cut equals the capacity of the min-cut
C
$|f| = c(S^*,T^*)$ but $f(S^*,T^*)$ could be less (some edges may carry less than capacity)
D
$f(S^*,T^*) = c(S^*,T^*)$ only if all edges crossing the cut are saturated, which may not happen
Answer: B — all three are equal.

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^*)$.
prog / daa / ch09 / q02 ★★ Edmonds-Karp Bound

Why does choosing BFS (shortest) augmenting paths guarantee $O(VE)$ augmentations, while DFS can require $O(|f^*|)$ augmentations?

A
BFS finds longer paths so each augmentation pushes more flow
B
DFS is slower per augmentation, so the total time is worse
C
With BFS, distances $\delta(v)$ can only increase; a critical edge $(u,v)$ requires $\delta(u)$ to increase by at least 2 before it can be critical again, bounding each edge to $O(V)$ critical events and total augmentations to $O(VE)$
D
BFS always saturates at least $E/V$ edges per augmentation, giving a better bound
Answer: C.

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)$.
prog / daa / ch09 / q03 ★★ Integrality

A flow network has all integer capacities. Ford-Fulkerson finds the max flow. Which statement is guaranteed by the integrality theorem?

A
The max flow value is an integer, but individual edge flows may be fractional
B
All edge flows are integers only if all capacities are the same value
C
There exists a maximum flow where every edge carries an integer amount of flow — enabling integer reductions like bipartite matching
D
The max flow value and all edge flows are always integers, regardless of the algorithm used
Answer: C — there exists an integer max flow.

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.
prog / daa / ch09 / q04 ★★★ König’s Theorem

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?

A
Any edge from $S\cap L$ to $T\cap R$ would cross the cut with infinite capacity, making the cut value infinite; so there are no such edges, meaning every $L$-$R$ edge is covered by either its left endpoint being in $T\cap L$ or its right endpoint being in $S\cap R$ — which is exactly the vertex cover
B
Infinite capacity edges are always removed before running max flow, simplifying the network
C
The min-cut must avoid $L$-$R$ edges because they represent matched pairs
D
Finite cuts only exist when the bipartite graph is a perfect matching
Answer: A.

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.

Terminal Questions — Chapter 09 10 problems · No answers given

Work through independently. Q1–3 direct application. Q4–7 synthesis. Q8–10 careful argument.

1
Run Edmonds-Karp by hand on the network with 6 nodes $\{s,a,b,c,d,t\}$ and edges: $s\to a(10)$, $s\to c(10)$, $a\to b(4)$, $a\to c(8)$, $b\to t(10)$, $c\to d(9)$, $d\to b(6)$, $d\to t(10)$. Show each BFS path and augmentation step. What is the max flow and which cut achieves the min cut? Easy
2
A bipartite graph has left vertices $\{1,2,3\}$ and right vertices $\{a,b,c,d\}$ with edges $\{1a,1b,2b,2c,3c,3d\}$. Find the maximum matching and minimum vertex cover using the max-flow reduction. Verify that König’s theorem holds. Easy
3
Demonstrate the adversarial case for Ford-Fulkerson with DFS: construct the 4-node graph with capacities $10^9$ and the middle edge of capacity 1. Trace the first 4 iterations of DFS-based augmentation to see the alternating path issue. Show that Edmonds-Karp (BFS) finds the max flow in exactly 2 augmentations. Easy
4
Implement Dinic’s algorithm (blocking flows on level graphs) which runs in $O(V^2 E)$ — faster than Edmonds-Karp’s $O(VE^2)$ for dense graphs. Test on the adversarial Ford-Fulkerson example and on a random bipartite matching. Medium
5
Model the following as a max-flow problem and solve: you have 4 NSE instruments (NIFTY, BANKNIFTY, RELIANCE, HDFC) and 3 strategies (straddle, strangle, spread). Each strategy can use any instrument, but each instrument can be in at most 2 strategies simultaneously. Maximise the number of active (instrument, strategy) assignments. Medium
6
Baseball elimination: team $i$ has $w_i$ wins, $r_i$ remaining games, and plays $g_{ij}$ games against team $j$. Team $i$ is eliminated if no matter how remaining games are played, team $i$ cannot finish first. Show how to reduce this to max flow: the max flow determines whether team $i$ is eliminatable. Implement and test on the example from CLRS. Medium
7
The circulation problem: given a directed graph with lower bounds $l(u,v) \leq f(u,v) \leq c(u,v)$ (each edge must carry at least $l$ flow), determine if a feasible flow exists. Reduce to standard max flow by transforming the lower bounds. What happens when some $l(u,v) > 0$? Medium
8
Prove the Monotonicity Lemma for Edmonds-Karp: after each augmentation, $\delta_f(s,v)$ can only increase. Use induction on $\delta_{f'}(s,v)$ (the new distance) and consider both cases: the edge $(u,v)$ was in $G_f$ before augmentation, or it was newly created as a reverse edge. Hard
9
Prove that in any bipartite graph, max matching size = min vertex cover size (König’s theorem) using the max-flow min-cut approach from R7. You need to show both directions: (a) any vertex cover defines a finite-capacity cut $\Rightarrow$ min cut $\leq$ min vertex cover; (b) any finite cut defines a vertex cover $\Rightarrow$ min vertex cover $\leq$ min cut. Hard
10
The max-flow problem has been a subject of decades of research. Summarise the history: Ford-Fulkerson (1956, $O(E|f^*|)$), Edmonds-Karp (1972, $O(VE^2)$), Dinic (1970, $O(V^2 E)$), King-Rao-Tarjan (1994, $O(VE\log_{E/V\log V} V)$), Orlin (2013, $O(VE)$). Why is the $O(VE)$ bound optimal for general graphs, and what special graph structures enable faster algorithms? Hard