DSA DSA In Depth
0%
lesson network-flowresidual-graphford-fulkerson

Flow Networks, Residual Capacities, Augmenting Paths & Ford-Fulkerson Method

7 min read Module 5.3: Network Flow, Min-Cut & Bipartite Matching Updated Oct 23, 2026
📄 Cheat Sheet
Ford-Fulkerson & Network Flow Fundamentals — Residual Graphs and Augmenting Paths
William Fiset Ford-Fulkerson & Network Flow Fundamentals — Residual Graphs and Augmenting Paths Watch on YouTube ↗

Demonstrates how residual capacities and reverse undo edges rescue greedy flow routing from suboptimal bottlenecks.

0:00 / --:--

Formal Flow Network Invariants & The Residual Graph $G_f$

A flow network $G = (V, E, c, s, t)$ is a directed graph with source $s \in V$, sink $t \in V$, and capacity function $c: V \times V \to \mathbb{R}_{\ge 0}$. An $s$-$t$ flow is a function $f: V \times V \to \mathbb{R}$ satisfying three axioms:

Formal Definition — Flow Network & Residual Capacity Invariants
  1. Capacity Constraint: For all $u, v \in V$, $0 \le f(u, v) \le c(u, v)$.
  2. Flow Conservation: For every internal node $u \in V \setminus \{s, t\}$, total incoming flow equals total outgoing flow: $$\sum_{v \in V} f(v, u) = \sum_{w \in V} f(u, w)$$
  3. Residual Capacity $c_f(u, v)$: Given flow $f$, the residual network $G_f = (V, E_f)$ contains all edges with strictly positive residual capacity $c_f(u, v) > 0$, defined for each directed edge $(u, v) \in E$ as: $$c_f(u, v) = c(u, v) - f(u, v) \quad (\text{forward unused capacity})$$ $$c_f(v, u) = f(u, v) \quad (\text{backward undo capacity})$$

Why Backward Edges Are Essential: The Diamond Rerouting Example

Consider the classic 4-vertex diamond network with $s = 0$, $a = 1$, $b = 2$, $t = 3$, where outer edges $(s,a), (a,t), (s,b), (b,t)$ all have capacity $10$, and the middle cross-edge $(a, b)$ has capacity $10$. Suppose the first DFS picks the zig-zag path $s \to a \to b \to t$ and pushes $\Delta = 10$ units of flow. Without a backward edge $(b, a)$, no forward path from $s$ to $t$ remains, stranding total flow at $10$ instead of the true optimum $20$.

With the residual graph $G_f$, pushing $10$ units along $(a, b)$ creates backward residual edge $(b, a)$ with $c_f(b, a) = 10$. The next search finds augmenting path $s \to b \to a \to t$ of bottleneck $\Delta = 10$. Traversing $(b, a)$ cancels the flow on $(a, b)$, yielding two parallel streams $s \to a \to t$ ($10$) and $s \to b \to t$ ($10$) for a total flow of $|f| = 20$!

Step 1: Push 10 on s → a → b → t Step 2: Augment 10 via backward edge b → a s a b t 10/10 10/10 10/10 s a b t undo b→a Total Flow |f| = 20 (Optimal)
flowchart LR Init["Initialize f(e) = 0 for all e in E"] --> Find["Search residual graph G_f for s ~> t path P with c_f(e) > 0"] Find --> Exists{"Path P found?"} Exists -- "Yes" --> Bottleneck["Compute Delta = min_{e in P} c_f(e)"] Bottleneck --> Augment["For each (u, v) in P: c_f(u,v) -= Delta, c_f(v,u) += Delta"] Augment --> Find Exists -- "No" --> Done["Return maximum flow |f|"]
Dinic’s Max-Flow: BFS Level Graph, Blocking Flow, S-T Min-Cut & Hopcroft-Karp
O(V²E) Dinic · O(E√V) Unit / Matching

Visualize Dinic’s BFS level graph L_G and current-arc DFS blocking flows alongside backward residual arcs (v→u), extract the saturated S-T minimum cut, or step through Hopcroft-Karp disjoint shortest augmenting paths.

Step 1 / 8
Custom Input:
Timeline

Invariant & State

Phase 1 Blocking Flow: Pushed 4 along S→A→C→T and 10 along S→{A,B}→D→T. Edges S→A, A→C, D→T saturated! Total flow = 14.
10/10 4/10 4/4 6/8 4/9 4/10 0/6 10/10 S lvl=0 A lvl=1 B lvl=1 C lvl=2 D lvl=2 T lvl=3
Total Max-Flow |f|
14
Dinic Phase
Phase 1 Blocking Flow (level[T] = 3)
S-T Min-Cut Capacity
c(S→A)=10 + c(B→D)=9 = 19
[Step 1] Phase 1 Blocking Flow: Pushed 4 along S→A→C→T and 10 along S→{A,B}→D→T. Edges S→A, A→C, D→T saturated! Total flow = 14.
Hardware Reality — Paired XOR Edge Indexing (e ^ 1) in Flat Arrays

In production flow solvers, searching an adjacency list to find the reverse edge $(v, u)$ corresponding to $(u, v)$ would cost $O(\text{deg}(v))$ per step. Instead, we store all edges in a flat std::vector<Edge> edges and always append forward and backward edges in adjacent pairs at indices $2k$ and $2k+1$. Then for any edge index id, its reverse residual partner is accessed in a single 1-cycle bitwise XOR instruction: id ^ 1!

Algorithm VariantPath SelectionTime ComplexityPolynomial?
Ford-Fulkerson (DFS)Arbitrary DFS path in $G_f$$O(E \cdot |f^*|)$Pseudo-polynomial (integer capacities)
Capacity ScalingPaths with $c_f(e) \ge \Delta$ ($\Delta = 2^{\lfloor \log_2 U \rfloor} \downarrow 1$)$O(E^2 \log U)$Weakly polynomial in max capacity $U$
Edmonds-Karp (BFS)Shortest unweighted path in $G_f$$O(V E^2)$Strongly polynomial

Production C++20 Implementation: Paired-Edge Residual Graph & Capacity Scaling

#include <vector>
#include <algorithm>
#include <limits>

class CapacityScalingFlow {
public:
    struct Arc {
        int to;
        long long cap; // Residual capacity c_f
    };

    explicit CapacityScalingFlow(int n) : numVertices(n), adj(n) {}

    void addEdge(int u, int v, long long capacity) {
        adj[u].push_back(static_cast<int>(arcs.size()));
        arcs.push_back({v, capacity}); // Forward edge at index 2k
        adj[v].push_back(static_cast<int>(arcs.size()));
        arcs.push_back({u, 0});        // Reverse edge at index 2k + 1 (id ^ 1)
        maxCap = std::max(maxCap, capacity);
    }

    long long maxFlow(int s, int t) {
        long long totalFlow = 0;
        long long threshold = 1;
        while (threshold <= maxCap / 2) threshold <<= 1;

        std::vector<uint8_t> visited(numVertices, 0);
        for (; threshold >= 1; threshold >>= 1) {
            while (true) {
                std::fill(visited.begin(), visited.end(), 0);
                long long pushed = dfs(s, t, std::numeric_limits<long long>::max(), threshold, visited);
                if (pushed == 0) break;
                totalFlow += pushed;
            }
        }
        return totalFlow;
    }

private:
    int numVertices;
    long long maxCap = 0;
    std::vector<Arc> arcs;
    std::vector<std::vector<int>> adj;

    long long dfs(int u, int t, long long flowIn, long long threshold, std::vector<uint8_t>& visited) {
        if (u == t) return flowIn;
        visited[u] = 1;
        for (int id : adj[u]) {
            int v = arcs[id].to;
            if (!visited[v] && arcs[id].cap >= threshold) {
                long long pushed = dfs(v, t, std::min(flowIn, arcs[id].cap), threshold, visited);
                if (pushed > 0) {
                    arcs[id].cap -= pushed;
                    arcs[id ^ 1].cap += pushed;
                    return pushed;
                }
            }
        }
        return 0;
    }
};
Failure Mode — The $O(E \cdot |f^*|)$ Middle-Edge Trap & Irrational Capacities

If outer diamond capacities are $U = 10^9$ and the middle cross-edge $(a, b)$ has capacity $1$, naive DFS Ford-Fulkerson alternates between $s \to a \to b \to t$ and $s \to b \to a \to t$, augmenting by $\Delta = 1$ per step for $2 \times 10^9$ iterations! Even worse, on irrational capacities (e.g., powers of the golden ratio $\phi$), Ford-Fulkerson can fail to terminate altogether and converge to a strictly suboptimal flow. Always use Capacity Scaling, Edmonds-Karp, or Dinic's algorithm.

Check yourself Question 1 of 4
Concept Check Role of Backward Residual Edges recall
Section: Residual Graph Mechanics

In the Ford-Fulkerson method, why do we add $\Delta$ units of capacity to the backward residual edge $(v, u)$ whenever we push $\Delta$ units of flow forward along edge $(u, v)$?

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

How does Capacity Scaling eliminate the $O(E |f^*|)$ pathology of DFS Ford-Fulkerson?

Key Takeaways

Every forward edge (u, v) with flow f(u, v) induces a backward residual edge (v, u) with residual capacity f(u, v), enabling greedy flow cancellation.
Storing forward and reverse arcs at consecutive indices (2k and 2k+1) in a flat vector allows O(1) partner lookup via bitwise XOR (id ^ 1).
Naive DFS Ford-Fulkerson runs in O(E |f*|) pseudo-polynomial time and can take 2 * 10^9 steps on a 4-node diamond with capacity 10^9.
Capacity Scaling restricts augmentations to edges with residual capacity >= Delta (halving Delta each phase), guaranteeing O(E^2 log U) polynomial runtime.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue