Loading learning content...
The true power of network flow lies not in solving flow problems directly, but in its ability to model an astonishing variety of seemingly unrelated optimization problems. Once you recognize a problem as a flow problem in disguise, you unlock polynomial-time solutions and deep theoretical insights.
This page explores the most important applications of maximum flow, from classic bipartite matching to sophisticated scheduling and allocation problems. Each reduction demonstrates the art of problem transformation—a skill that separates algorithm designers from algorithm users.
By the end of this page, you'll understand how to: reduce bipartite matching to max flow, model assignment problems as flow networks, solve resource allocation with capacity constraints, recognize flow patterns in scheduling problems, and apply network flow to real-world scenarios from organ donation to project management.
The Maximum Bipartite Matching Problem:
Given a bipartite graph G = (X ∪ Y, E) where edges only connect vertices in X to vertices in Y, find the largest set of edges M ⊆ E such that no two edges in M share a vertex.
Real-World Examples:
The Key Insight: Maximum bipartite matching reduces directly to maximum flow!
The Reduction:
1 [x₁]----1----[y₁] 1
↗ \ / ↘
[S] \ / [T]
↘ / \ ↗
1 [x₂]----1----[y₂] 1
Why This Works:
Because all capacities are 1, the maximum flow is an integer. More importantly, each edge either carries flow 0 or 1—never fractional. This means the flow directly gives a valid matching: matched pairs are precisely the edges with flow 1.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
def max_bipartite_matching(X, Y, edges): """ Find maximum bipartite matching using max flow reduction. Args: X: List of vertices on left side Y: List of vertices on right side edges: List of (x, y) pairs representing allowed matchings Returns: List of matched pairs """ from collections import deque # Build flow network SOURCE = 'SOURCE' SINK = 'SINK' # Capacity graph: graph[u][v] = capacity graph = {SOURCE: {}, SINK: {}} for x in X: graph[SOURCE][x] = 1 graph[x] = {} for y in Y: graph[y] = {SINK: 1} for x, y in edges: graph[x][y] = 1 graph[SINK] = {} # Run Edmonds-Karp (max flow) def bfs_augment(): parent = {SOURCE: None} queue = deque([SOURCE]) while queue: u = queue.popleft() for v, cap in graph[u].items(): if v not in parent and cap > 0: parent[v] = u if v == SINK: # Found augmenting path path = [] curr = SINK while parent[curr] is not None: path.append((parent[curr], curr)) curr = parent[curr] return path queue.append(v) return None # Augment flow max_flow = 0 while True: path = bfs_augment() if path is None: break # Augment by 1 (all capacities are 1) for u, v in path: graph[u][v] -= 1 if v not in graph: graph[v] = {} graph[v][u] = graph.get(v, {}).get(u, 0) + 1 max_flow += 1 # Extract matching: edges from X to Y with flow (i.e., capacity exhausted) matching = [] for x in X: for y in Y: if (x, y) in [(u, v) for u, v in edges]: # If original capacity 1 is now 0, edge is matched if graph[x].get(y, 0) == 0 and y in edges_dict.get(x, set()): matching.append((x, y)) # Cleaner extraction: check reverse edges matching = [] for x in X: for y in Y: if graph.get(y, {}).get(x, 0) > 0: # Reverse edge exists with flow matching.append((x, y)) return matching # Example: Job assignmentworkers = ['Alice', 'Bob', 'Charlie']jobs = ['Frontend', 'Backend', 'DevOps']skills = [ ('Alice', 'Frontend'), ('Alice', 'Backend'), ('Bob', 'Backend'), ('Bob', 'DevOps'), ('Charlie', 'Frontend'),] matching = max_bipartite_matching(workers, jobs, skills)print(f"Maximum matching: {matching}")# Possible output: [('Alice', 'Backend'), ('Bob', 'DevOps'), ('Charlie', 'Frontend')]Basic bipartite matching is binary: either an edge exists or it doesn't. But many real problems have preferences or costs.
The Assignment Problem:
Given n workers and n jobs, where worker i doing job j has cost c(i, j), find a one-to-one assignment minimizing total cost.
The Minimum-Cost Maximum-Flow Variation:
Max flow with costs assigns not just capacities but also per-unit costs to edges. The goal: find maximum flow with minimum total cost.
Reduction for assignment:
Minimum-cost max-flow finds the assignment minimizing total cost while ensuring everyone is assigned.
Minimum-cost maximum-flow requires different algorithms: successive shortest paths, cycle-canceling, or cost-scaling. The key insight is that it's still polynomial-time solvable, extending the power of flow-based approaches to weighted optimization.
Example: Hospital-Resident Matching
The National Resident Matching Program pairs medical residents with hospitals:
While the full stable matching problem uses the Gale-Shapley algorithm, capacity-constrained variants can be modeled as flow:
Example: Organ Donation Networks
Kidney exchange programs match donor-recipient pairs:
| Problem | Reduction Approach | Algorithm |
|---|---|---|
| Simple matching | Max flow with unit capacities | Edmonds-Karp / Dinic |
| Weighted matching | Min-cost max flow | Hungarian / Successive shortest paths |
| Many-to-one matching | Source/sink capacities > 1 | Standard max flow |
| Constrained matching | Add edges only for valid pairs | Max flow on restricted graph |
Many optimization problems involve distributing limited resources to competing demands. Network flow provides elegant solutions.
The General Resource Allocation Framework:
Example: Supply Chain Optimization
A company has:
Network Model:
┌─────┐ ┌───────┐
┌─────────►│ DC1 ├────────►│Store1 │
│ 100 └──┬──┘ 50 └───────┘
┌─────┐ │ │
│Fact1├───┤ │80 ┌───────┐
└─────┘ │ ├───────►│Store2 │
200 │ ┌───┴──┐ └───────┘
└────────►│ DC2 │
┌─────┐ 120 └──┬───┘ ┌───────┐
│Fact2├───────────────►│────────►│Store3 │
└─────┘ direct │ 60 └───────┘
150 │
│ ┌───────┐
┌─────┐ 90 └────────►│Store4 │
│Fact3├───────────────────► └───────┘
└─────┘ 70
Maximum flow tells us the maximum goods deliverable. The min-cut reveals which links to upgrade for higher throughput.
If different product types share capacity (e.g., trucks carry mixed goods), the problem becomes multi-commodity flow—significantly harder (often NP-hard). For single commodity or separable problems, standard max flow suffices.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
def model_supply_chain(factories, warehouses, stores, links): """ Model a supply chain as a flow network. Args: factories: Dict[name, capacity] - production capacity warehouses: Dict[name, capacity] - handling capacity stores: Dict[name, demand] - store demands links: List[(from, to, capacity)] - transportation links Returns: Flow network graph suitable for max flow algorithms """ SOURCE = 'SUPER_SOURCE' SINK = 'SUPER_SINK' graph = {SOURCE: {}, SINK: {}} # Super-source connects to factories for factory, capacity in factories.items(): graph[SOURCE][factory] = capacity graph[factory] = {} # Warehouses need vertex splitting for handling capacity for wh, capacity in warehouses.items(): wh_in = f"{wh}_IN" wh_out = f"{wh}_OUT" graph[wh_in] = {wh_out: capacity} graph[wh_out] = {} # Stores connect to super-sink (with demand as capacity) for store, demand in stores.items(): graph[store] = {SINK: demand} # Add transportation links for src, dst, cap in links: # Handle warehouse splits actual_src = f"{src}_OUT" if src in warehouses else src actual_dst = f"{dst}_IN" if dst in warehouses else dst if actual_src not in graph: graph[actual_src] = {} graph[actual_src][actual_dst] = cap return graph, SOURCE, SINK # Example usagefactories = {'Factory_A': 200, 'Factory_B': 150, 'Factory_C': 100}warehouses = {'DC_North': 180, 'DC_South': 200}stores = {'Store_1': 80, 'Store_2': 100, 'Store_3': 120, 'Store_4': 90}links = [ ('Factory_A', 'DC_North', 150), ('Factory_A', 'DC_South', 100), ('Factory_B', 'DC_North', 80), ('Factory_B', 'DC_South', 120), ('Factory_C', 'DC_South', 100), ('DC_North', 'Store_1', 60), ('DC_North', 'Store_2', 70), ('DC_South', 'Store_2', 50), ('DC_South', 'Store_3', 100), ('DC_South', 'Store_4', 80),] graph, source, sink = model_supply_chain(factories, warehouses, stores, links)# Now run max_flow(graph, source, sink) to find maximum deliverable goodsScheduling problems often reduce to network flow when constraints are capacity-like.
The Crew Scheduling Problem:
Airline needs to assign crews to flights:
Flow Modeling:
The Meeting Room Problem:
Schedule n meetings in k rooms:
As Bipartite Matching:
The Sports Tournament Problem (Baseball Elimination):
Can team X still win the championship?
Flow formulation reveals impossibility:
Often we don't need the actual flow value—just whether a target is achievable. In scheduling, max flow = required resources means the schedule is feasible. This binary check uses the same algorithm but answers a yes/no question.
| Problem | Left Side (Sources) | Right Side (Sinks) | Edge Meaning |
|---|---|---|---|
| Crew scheduling | Crew members | Flights | Crew qualified for flight |
| Room scheduling | Meetings | Room-time slots | Meeting fits in slot |
| Exam scheduling | Exams | Time slots | No student conflict |
| Sports elimination | Games to play | Teams | Game winner |
The Project Selection Problem:
Given n projects with profits p₁, ..., pₙ (some negative, representing costs) and dependencies (if you do project i, you must also do project j), find the subset maximizing total profit.
Why Dependencies Matter:
Project A (profit $100) requires prerequisite Project B (cost $30).
The optimal is taking both. Dependencies create constraints that flow models elegantly.
The Flow Reduction:
The Interpretation:
A min cut (S, T) corresponds to a project selection:
Cut capacity = lost positive profits + incurred negative costs
Maximum profit = (sum of all positive profits) - (min cut capacity)
The min cut naturally respects dependencies (infinite capacity edges prevent violations) and optimizes profit (minimizing cut = maximizing retained profits minus costs). This reduction transforms a combinatorial optimization problem into a graph problem.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
def max_profit_projects(projects, dependencies): """ Find maximum profit subset of projects respecting dependencies. Args: projects: Dict[project_id, profit] - profit (negative = cost) dependencies: List[(required, dependent)] - if take dependent, must take required Returns: Tuple of (max_profit, selected_projects) """ SOURCE = 'SOURCE' SINK = 'SINK' INF = float('inf') # Build graph graph = {SOURCE: {}, SINK: {}} for project in projects: graph[project] = {} # Source edges for positive profits, sink edges for costs total_positive = 0 for project, profit in projects.items(): if profit > 0: graph[SOURCE][project] = profit total_positive += profit elif profit < 0: graph[project][SINK] = -profit # Dependency edges (j required by i means edge j -> i) for required, dependent in dependencies: graph[required][dependent] = INF # Run min cut (= max flow) min_cut_value = max_flow(graph, SOURCE, SINK) # Use any max flow algorithm # Find selected projects (those in source-side of min cut) # Run BFS from source in residual graph selected = find_source_reachable(residual_graph, SOURCE) selected.remove(SOURCE) max_profit = total_positive - min_cut_value return max_profit, selected # Exampleprojects = { 'Website': 100, # Revenue $100 'Backend': -40, # Cost $40 'Database': -20, # Cost $20 'Analytics': 50, # Revenue $50 'ML_Model': 80, # Revenue $80 'Data_Pipeline': -30 # Cost $30} dependencies = [ ('Backend', 'Website'), # Website requires Backend ('Database', 'Backend'), # Backend requires Database ('Data_Pipeline', 'ML_Model'), # ML requires Data Pipeline ('Database', 'Analytics'), # Analytics requires Database] # Optimal: Take Website, Backend, Database, Analytics# = 100 - 40 - 20 + 50 = $90# (Skip ML_Model+Data_Pipeline: 80-30=50, but we already have 90)One of the most elegant applications of max-flow min-cut is in computer vision for image segmentation—separating foreground objects from background.
The Setup:
The Network Model:
Why Min-Cut Works:
The minimum cut partitions pixels into S (foreground) and T (background):
Min cut = minimum total penalty
This naturally:
The Algorithm:
1. For each pixel, compute foreground and background likelihood
2. Build the graph with appropriate edge weights
3. Compute min cut = max flow
4. Pixels reachable from source in residual graph = foreground
5. Others = background
In practice, users draw "scribbles" marking definite foreground and background. These become infinite-capacity edges to source/sink. The algorithm then finds the optimal boundary respecting user input—a perfect blend of human guidance and algorithmic optimization.
Real-World Applications:
Max-flow min-cut provides an algorithmic proof of Hall's Marriage Theorem, a classical result in combinatorics.
Hall's Theorem:
A bipartite graph G = (X ∪ Y, E) has a matching that saturates all of X (every vertex in X is matched) if and only if for every subset S ⊆ X:
|N(S)| ≥ |S|
where N(S) is the neighborhood of S (all vertices in Y adjacent to some vertex in S).
The Condition Intuition:
If there's a subset of, say, 5 elements in X that collectively only connect to 4 elements in Y, we can't match all 5—there aren't enough "partners." Hall's condition says this bottleneck never exists.
Proof via Max-Flow Min-Cut:
If Hall's condition holds:
By Hall's condition, |N(A)| ≥ |A|. The cut must "pay" for each vertex in A either by including its neighbor in B or by cutting the edge.
This forces min cut ≥ |X|, hence max flow ≥ |X|, meaning a matching saturating X exists.
If matching exists:
A related result: in bipartite graphs, maximum matching size = minimum vertex cover size. This too follows from max-flow min-cut. The duality between matchings and covers mirrors the duality between flows and cuts.
| Theorem | Statement | Connection to Flow |
|---|---|---|
| Hall's Marriage | Perfect matching ↔ Hall's condition | Min cut ≥ |X| ↔ Hall's condition |
| König's | Max matching = min vertex cover (bipartite) | Direct duality |
| Menger's | Max disjoint paths = min vertex cut | Unit capacity flow |
| Dilworth's | Min chain cover = max antichain | DAG path cover reduction |
Menger's Theorem connects path connectivity to cut size, and max flow provides an algorithmic proof.
Edge-Disjoint Paths:
The maximum number of edge-disjoint paths from s to t equals the minimum number of edges whose removal disconnects s from t.
Proof via Max Flow:
Vertex-Disjoint Paths:
The maximum number of vertex-disjoint paths (sharing no vertices except s and t) equals the minimum number of vertices whose removal disconnects s from t.
Proof via Vertex Splitting:
Applications of Disjoint Paths:
Network Reliability:
Routing:
Security:
Edge-disjoint paths, vertex-disjoint paths, connectivity, and reliability all reduce to max flow. The flow value tells you the robustness; the min cut tells you the vulnerabilities. One algorithm, many insights.
We've explored the rich tapestry of problems solvable via network flow. Let's consolidate the key insights:
The Reduction Mindset:
The most valuable skill from this module isn't memorizing these reductions—it's developing the reduction mindset. When you encounter an optimization problem, ask:
If the answers align with flow structure, you've found a polynomial-time solution. This pattern recognition is what makes network flow a powerful tool in your algorithmic toolkit.
Congratulations! You've completed the Network Flow — Conceptual Introduction module. You now understand flow networks, the Ford-Fulkerson method, the max-flow min-cut theorem, and a wealth of applications. These concepts form the foundation for advanced topics like minimum-cost flow, multi-commodity flow, and approximation algorithms for NP-hard problems.
What's Next in Your Journey:
Network flow opens doors to advanced algorithmic techniques:
The concepts you've learned here are foundational—they'll appear again and again as you advance in algorithmic problem-solving.