Maximum Flow - Ford-Fulkerson
Key Points
- ā¢FordāFulkerson finds the maximum possible flow from a source to a sink by repeatedly pushing flow along an augmenting path in the residual graph.
- ā¢The residual graph tracks how much more flow can go forward and how much already-sent flow can be sent back to reroute if needed.
- ā¢An augmenting path increases total flow by the pathās bottleneck capacity, and we repeat until no such path exists.
- ā¢If capacities are integers, basic FordāFulkerson with DFS runs in O(E Ā· F) time, where F is the max-flow value.
- ā¢EdmondsāKarp is the BFS variant that guarantees O(V Ā· ) time by always choosing the shortest (in edges) augmenting path.
- ā¢Capacity scaling improves performance when capacities are large by only searching edges with capacity at least a threshold that halves each phase.
- ā¢After computing max flow, the reachable set in the residual graph gives a minimum sāt cut by the Max-Flow Min-Cut Theorem.
- ā¢Use long long and always add reverse edges to avoid overflow and correctness bugs in C++ implementations.
Prerequisites
- āGraphs and adjacency lists ā Max-flow algorithms operate on directed graphs and require efficient edge storage.
- āBreadth-first search (BFS) and depth-first search (DFS) ā FordāFulkerson uses path searches; EdmondsāKarp specifically relies on BFS.
- āBig-O notation and asymptotic analysis ā Understanding performance guarantees and trade-offs requires analyzing O(VĀ·E^2) vs O(EĀ·F), etc.
- āBasic C++ data structures (vectors, queues) ā Implementing residual graphs and BFS/DFS efficiently depends on these containers.
- āMathematical reasoning with sums and minima ā Residual capacities, bottlenecks, and cuts use simple but precise definitions and proofs.
Detailed Explanation
Tap terms for definitions01Overview
Maximum flow is about pushing as much āstuffā as possible from a source node to a sink node through a directed network with capacities on edges. The FordāFulkerson method is a simple and powerful approach: as long as there exists a path from source to sink where every edge still has remaining capacity, we can increase the flow by the smallest remaining capacity along that path (the bottleneck). To support this, we maintain a residual graph: forward edges show how much more we can push; reverse edges show how much of the current flow we can cancel to reroute it. Repeating this process eventually reaches an optimal flow, proven by the Max-Flow Min-Cut Theorem. Different ways of finding augmenting paths give different running times: depth-first search (DFS) is often fast in practice but has only a weak theoretical bound dependent on the max-flow value, while breadth-first search (BFS) in EdmondsāKarp guarantees polynomial time. Capacity scaling further improves performance when capacities are large by focusing on ābigā edges first. Max flow underlies many problems such as bipartite matching, disjoint paths, scheduling with capacity constraints, and minimum cuts in images and graphs.
02Intuition & Analogies
Imagine a cityās water network: a reservoir (source) must deliver as much water as possible to a treatment plant (sink) through pipes (directed edges) that each have a maximum safe flow (capacity). Initially, no water flows. You look for a route of pipes from the reservoir to the plant such that each pipe still has spare room; send as much as the tightest pipe on that route allows. That tightest pipe is the bottleneck. Do this again and again. Sometimes, after sending water along one set of routes, you realize you could do better by partially undoing (sending water back) through some pipes and redirecting along a new, more efficient route. The āability to send backā is modeled by reverse edges in the residual graphālike adjustable valves that can reclaim already-used capacity to improve the overall plan. Using DFS to find routes is like exploring one tunnel deeply; using BFS (EdmondsāKarp) is like always choosing the route with the fewest pipe segments, which tends to avoid wasting effort on long detours. Capacity scaling is like first installing major water mains (big-capacity routes) before fiddling with small garden hoses, which speeds up getting most of the water through quickly. When you canāt find any route with spare capacity, youāve implicitly identified a choke boundary (a cut) whose total pipe capacity equals the total water delivered, guaranteeing optimality.
03Formal Definition
04When to Use
Use FordāFulkerson and its variants whenever you must route as much as possible through a capacity-limited network. Common applications include: (1) Bipartite matching (convert to a flow network; maximum matching equals max flow). (2) Edge- or vertex-disjoint paths (ensure each edge or vertex has unit capacity). (3) Scheduling with resource limits (time slots as edges with capacities). (4) Project selection and minimum cut formulations (separate profitable and unprofitable choices with constraints). (5) Image segmentation and graph cuts (min sāt cut yields foreground/background separation). For contest problems: (a) If graphs are modest (e.g., up to a few thousand edges) and capacities are small integers, DFS-based FordāFulkerson often passes. (b) For safer polynomial guarantees, use EdmondsāKarp (BFS). (c) If capacities vary widely or are large, capacity scaling can be significantly faster in practice. If graphs are large and dense or extremely performance-sensitive, consider Dinicās algorithmābut understanding FordāFulkerson first is essential.
ā ļøCommon Mistakes
- Forgetting reverse edges: Without adding a reverse edge of initial capacity 0 for every forward edge, you cannot cancel and reroute flow, breaking correctness. Always store forward and reverse edges with cross indices. - Using 32-bit int for capacities/flow: Sums can overflow. Prefer long long (64-bit) in C++. - Not resetting visited arrays per augmentation: DFS/BFS must consider a fresh search each iteration; otherwise, you may miss valid paths. - Ignoring parallel edges: Combine or store both; if you overwrite one with another, capacity is lost. - Mishandling 0-capacity edges: Keep them but they should not be traversed in residual searches (cap > 0 checks). - Mixing 0-based and 1-based node indices: Standardize input handling and document it. - Recursion depth: Deep DFS on large graphs can overflow the stack; consider iterative DFS/BFS or increase stack if necessary. - Forgetting to record original capacities when you need to output a min cut: Residual capacities change; keep a separate list for reporting the cut. - Termination with non-integer capacities: The basic DFS strategyās O(EĀ·F) bound assumes integer capacities; with irrationals, careless implementations may cycle. Use BFS (EdmondsāKarp) or rational/integer capacities for guarantees.
Key Formulas
Capacity constraint
Explanation: Flow on any edge cannot exceed its capacity and cannot be negative. This ensures physical limits are respected.
Skew symmetry
Explanation: Sending x units from u to v is equivalent to sending āx from v to u. This makes reverse edges naturally represent cancellation.
Flow conservation
Explanation: Every intermediate node forwards exactly what it receives. Only the source emits and the sink absorbs net flow.
Value of a flow
Explanation: The total flow is the net outflow from the source, which also equals the net inflow to the sink. It measures how much is delivered.
Residual capacity
Explanation: Forward residual is unused capacity; backward residual is how much you can cancel. Residual edges define where more flow can go.
Bottleneck on a path
Explanation: The maximum extra flow we can push along an augmenting path P equals the smallest residual capacity on that path.
Augmentation update
Explanation: Increase flow by Ī on forward edges of P and decrease by Ī on reverse edges. This preserves feasibility and increases total flow by Ī.
Cut capacity
Explanation: The capacity of cut (S,T) sums capacities of edges leaving S. This bounds any sāt flow from above.
Max-Flow Min-Cut Theorem
Explanation: The maximum achievable flow equals the minimum cut capacity. When no augmenting path exists, you have reached this equality.
FordāFulkerson (DFS) time
Explanation: With integer capacities, each augmentation increases flow by at least 1, and each augmentation costs O(E) to find and update, totaling O(EĀ·F).
EdmondsāKarp time
Explanation: Each BFS-based augmentation is O(E), and the number of augmentations is O(VĀ·E) because the shortest-path distance to t increases at most O(VĀ·E) times.
Capacity scaling time
Explanation: Restricting searches to edges with residual ā„ and halving each phase yields O() work per phase times O( U) phases, where U is the max capacity.
Complexity Analysis
Code Examples
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 struct Edge { 5 int to; // destination vertex 6 long long cap; // residual capacity on this edge 7 int rev; // index of the reverse edge in g[to] 8 }; 9 10 struct MaxFlowDFS { 11 int n; 12 vector<vector<Edge>> g; // residual graph 13 14 MaxFlowDFS(int n): n(n), g(n) {} 15 16 void add_edge(int u, int v, long long c) { 17 // forward edge u->v with capacity c 18 Edge a{v, c, (int)g[v].size()}; 19 // reverse edge v->u with initial 0 capacity 20 Edge b{u, 0, (int)g[u].size()}; 21 g[u].push_back(a); 22 g[v].push_back(b); 23 } 24 25 long long dfs(int v, int t, long long f, vector<int>& vis) { 26 if (v == t) return f; // reached sink, can push f 27 vis[v] = 1; 28 for (auto &e : g[v]) { 29 if (!vis[e.to] && e.cap > 0) { 30 long long pushed = dfs(e.to, t, min(f, e.cap), vis); 31 if (pushed > 0) { 32 // use residual capacities to update 33 e.cap -= pushed; // reduce forward residual 34 g[e.to][e.rev].cap += pushed; // increase reverse residual 35 return pushed; 36 } 37 } 38 } 39 return 0; // no augmenting path from this vertex 40 } 41 42 long long max_flow(int s, int t) { 43 long long flow = 0; 44 while (true) { 45 vector<int> vis(n, 0); // must reset each augmentation 46 long long pushed = dfs(s, t, LLONG_MAX, vis); 47 if (pushed == 0) break; // no more augmenting paths 48 flow += pushed; 49 } 50 return flow; 51 } 52 }; 53 54 int main() { 55 ios::sync_with_stdio(false); 56 cin.tie(nullptr); 57 58 int n, m; // number of vertices and edges 59 if (!(cin >> n >> m)) return 0; 60 MaxFlowDFS mf(n); 61 for (int i = 0; i < m; ++i) { 62 int u, v; long long c; 63 cin >> u >> v >> c; // assume 0-based vertices 64 mf.add_edge(u, v, c); 65 } 66 int s, t; cin >> s >> t; 67 cout << mf.max_flow(s, t) << "\n"; 68 return 0; 69 } 70
This program implements the classic FordāFulkerson method using recursive DFS to find augmenting paths. The residual graph stores both forward and reverse edges. Each DFS call attempts to push as much flow as possible to the sink, bounded by encountered residual capacities; when it reaches the sink, we propagate the bottleneck upward and update residual capacities along the path. We repeat until DFS can no longer reach t from s. This is simple and often fast on small instances with small integer capacities.
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 struct Edge { 5 int to; 6 long long cap; 7 int rev; 8 }; 9 10 struct EdmondsKarp { 11 int n; 12 vector<vector<Edge>> g; 13 14 EdmondsKarp(int n): n(n), g(n) {} 15 16 void add_edge(int u, int v, long long c) { 17 Edge a{v, c, (int)g[v].size()}; 18 Edge b{u, 0, (int)g[u].size()}; 19 g[u].push_back(a); 20 g[v].push_back(b); 21 } 22 23 long long bfs(int s, int t, vector<int>& pv, vector<int>& pe) { 24 fill(pv.begin(), pv.end(), -1); 25 fill(pe.begin(), pe.end(), -1); 26 queue<int> q; 27 q.push(s); 28 pv[s] = s; // mark source as visited with itself as parent 29 while (!q.empty()) { 30 int v = q.front(); q.pop(); 31 if (v == t) break; 32 for (int i = 0; i < (int)g[v].size(); ++i) { 33 const Edge &e = g[v][i]; 34 if (pv[e.to] == -1 && e.cap > 0) { 35 pv[e.to] = v; // parent vertex 36 pe[e.to] = i; // index of edge used from parent to this node 37 q.push(e.to); 38 } 39 } 40 } 41 if (pv[t] == -1) return 0; // no path 42 long long add = LLONG_MAX; 43 // find bottleneck by walking back from t 44 for (int v = t; v != s; v = pv[v]) { 45 int u = pv[v]; 46 int ei = pe[v]; 47 add = min(add, g[u][ei].cap); 48 } 49 // apply augmentation 50 for (int v = t; v != s; v = pv[v]) { 51 int u = pv[v]; 52 int ei = pe[v]; 53 int ri = g[u][ei].rev; 54 g[u][ei].cap -= add; 55 g[v][ri].cap += add; 56 } 57 return add; 58 } 59 60 long long max_flow(int s, int t) { 61 vector<int> pv(n), pe(n); 62 long long flow = 0; 63 while (true) { 64 long long pushed = bfs(s, t, pv, pe); 65 if (pushed == 0) break; 66 flow += pushed; 67 } 68 return flow; 69 } 70 }; 71 72 int main() { 73 ios::sync_with_stdio(false); 74 cin.tie(nullptr); 75 76 int n, m; if (!(cin >> n >> m)) return 0; 77 EdmondsKarp ek(n); 78 for (int i = 0; i < m; ++i) { 79 int u, v; long long c; cin >> u >> v >> c; // 0-based 80 ek.add_edge(u, v, c); 81 } 82 int s, t; cin >> s >> t; 83 cout << ek.max_flow(s, t) << "\n"; 84 return 0; 85 } 86
This is the BFS variant (EdmondsāKarp). BFS finds the shortest augmenting path in terms of edges, ensuring a polynomial number of augmentations. The parent arrays (pv, pe) record how we reached each node; reconstructing from t to s yields the bottleneck, then we update forward and reverse residual capacities along the path.
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 struct Edge { 5 int to; 6 long long cap; 7 int rev; 8 }; 9 10 struct ScalingMaxFlow { 11 int n; 12 vector<vector<Edge>> g; 13 long long U = 0; // track maximum capacity added 14 15 ScalingMaxFlow(int n): n(n), g(n) {} 16 17 void add_edge(int u, int v, long long c) { 18 Edge a{v, c, (int)g[v].size()}; 19 Edge b{u, 0, (int)g[u].size()}; 20 g[u].push_back(a); 21 g[v].push_back(b); 22 U = max(U, c); 23 } 24 25 // BFS that only follows edges with residual >= delta 26 long long bfs_with_threshold(int s, int t, long long delta, vector<int>& pv, vector<int>& pe) { 27 fill(pv.begin(), pv.end(), -1); 28 fill(pe.begin(), pe.end(), -1); 29 queue<int> q; q.push(s); pv[s] = s; 30 while (!q.empty()) { 31 int v = q.front(); q.pop(); 32 if (v == t) break; 33 for (int i = 0; i < (int)g[v].size(); ++i) { 34 const Edge &e = g[v][i]; 35 if (pv[e.to] == -1 && e.cap >= delta) { 36 pv[e.to] = v; pe[e.to] = i; q.push(e.to); 37 } 38 } 39 } 40 if (pv[t] == -1) return 0; 41 long long add = LLONG_MAX; 42 for (int v = t; v != s; v = pv[v]) { 43 int u = pv[v]; int ei = pe[v]; 44 add = min(add, g[u][ei].cap); 45 } 46 for (int v = t; v != s; v = pv[v]) { 47 int u = pv[v]; int ei = pe[v]; int ri = g[u][ei].rev; 48 g[u][ei].cap -= add; 49 g[v][ri].cap += add; 50 } 51 return add; 52 } 53 54 long long max_flow(int s, int t) { 55 long long flow = 0; 56 if (U == 0) return 0; 57 long long delta = 1; 58 while (delta <= U) delta <<= 1; // delta = 2^k > U 59 delta >>= 1; // largest power of two <= U 60 vector<int> pv(n), pe(n); 61 while (delta > 0) { 62 while (true) { 63 long long pushed = bfs_with_threshold(s, t, delta, pv, pe); 64 if (pushed == 0) break; 65 flow += pushed; 66 } 67 delta >>= 1; // decrease threshold 68 } 69 return flow; 70 } 71 }; 72 73 int main() { 74 ios::sync_with_stdio(false); 75 cin.tie(nullptr); 76 77 int n, m; if (!(cin >> n >> m)) return 0; 78 ScalingMaxFlow mf(n); 79 for (int i = 0; i < m; ++i) { 80 int u, v; long long c; cin >> u >> v >> c; // 0-based 81 mf.add_edge(u, v, c); 82 } 83 int s, t; cin >> s >> t; 84 cout << mf.max_flow(s, t) << "\n"; 85 return 0; 86 } 87
Capacity scaling prefers augmenting along large-capacity edges first by enforcing a threshold Ī that halves each phase. BFS is restricted to edges with residual capacity at least Ī, which reduces the number of small, unproductive pushes when capacities vary widely. The resulting algorithm keeps the same residual-graph structure and correctness guarantees while often improving runtime in practice.
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 struct Edge { 5 int to; 6 long long cap; 7 int rev; 8 }; 9 10 struct EdmondsKarpWithCut { 11 int n; 12 vector<vector<Edge>> g; 13 vector<tuple<int,int,long long>> original; // (u,v,c) for reporting min cut 14 15 EdmondsKarpWithCut(int n): n(n), g(n) {} 16 17 void add_edge(int u, int v, long long c) { 18 Edge a{v, c, (int)g[v].size()}; 19 Edge b{u, 0, (int)g[u].size()}; 20 g[u].push_back(a); 21 g[v].push_back(b); 22 original.emplace_back(u, v, c); 23 } 24 25 long long bfs(int s, int t, vector<int>& pv, vector<int>& pe) { 26 fill(pv.begin(), pv.end(), -1); 27 fill(pe.begin(), pe.end(), -1); 28 queue<int> q; q.push(s); pv[s] = s; 29 while (!q.empty()) { 30 int v = q.front(); q.pop(); 31 if (v == t) break; 32 for (int i = 0; i < (int)g[v].size(); ++i) { 33 const Edge &e = g[v][i]; 34 if (pv[e.to] == -1 && e.cap > 0) { 35 pv[e.to] = v; pe[e.to] = i; q.push(e.to); 36 } 37 } 38 } 39 if (pv[t] == -1) return 0; 40 long long add = LLONG_MAX; 41 for (int v = t; v != s; v = pv[v]) { 42 int u = pv[v]; int ei = pe[v]; 43 add = min(add, g[u][ei].cap); 44 } 45 for (int v = t; v != s; v = pv[v]) { 46 int u = pv[v]; int ei = pe[v]; int ri = g[u][ei].rev; 47 g[u][ei].cap -= add; 48 g[v][ri].cap += add; 49 } 50 return add; 51 } 52 53 long long max_flow(int s, int t) { 54 vector<int> pv(n), pe(n); 55 long long flow = 0; 56 while (true) { 57 long long pushed = bfs(s, t, pv, pe); 58 if (pushed == 0) break; 59 flow += pushed; 60 } 61 return flow; 62 } 63 64 vector<int> min_cut_reachable(int s) { 65 // BFS on residual graph to find vertices reachable from s 66 vector<int> vis(n, 0); queue<int> q; q.push(s); vis[s] = 1; 67 while (!q.empty()) { 68 int v = q.front(); q.pop(); 69 for (auto &e : g[v]) { 70 if (!vis[e.to] && e.cap > 0) { 71 vis[e.to] = 1; q.push(e.to); 72 } 73 } 74 } 75 return vis; // vis[v]=1 means v in S; otherwise in T 76 } 77 }; 78 79 int main() { 80 ios::sync_with_stdio(false); 81 cin.tie(nullptr); 82 83 int n, m; if (!(cin >> n >> m)) return 0; 84 EdmondsKarpWithCut solver(n); 85 for (int i = 0; i < m; ++i) { 86 int u, v; long long c; cin >> u >> v >> c; // 0-based 87 solver.add_edge(u, v, c); 88 } 89 int s, t; cin >> s >> t; 90 long long maxflow = solver.max_flow(s, t); 91 vector<int> reach = solver.min_cut_reachable(s); 92 93 // Report min cut partition and crossing edges (u in S, v in T) 94 long long cutCap = 0; 95 vector<pair<int,int>> cutEdges; 96 for (auto [u, v, c] : solver.original) { 97 if (reach[u] && !reach[v] && c > 0) { 98 cutCap += c; 99 cutEdges.emplace_back(u, v); 100 } 101 } 102 103 cout << "max_flow " << maxflow << "\n"; 104 cout << "min_cut_capacity " << cutCap << "\n"; 105 cout << "S: "; 106 for (int i = 0; i < n; ++i) if (reach[i]) cout << i << ' '; 107 cout << "\nT: "; 108 for (int i = 0; i < n; ++i) if (!reach[i]) cout << i << ' '; 109 cout << "\ncut_edges_count " << cutEdges.size() << "\n"; 110 for (auto [u, v] : cutEdges) cout << u << ' ' << v << "\n"; 111 return 0; 112 } 113
We compute max flow with EdmondsāKarp, then run a BFS on the residual graph to find the set S of vertices reachable from s. By the Max-Flow Min-Cut Theorem, the edges from S to its complement T (in the original graph) that cross the partition and have positive original capacity form a minimum sāt cut, and their total capacity equals the max flow. The program prints the max flow, the min-cut capacity, the partition, and the list of cut edges.