DSA DSA In Depth
0%
lesson edmonds-karpmax-flowbfs

Edmonds-Karp Algorithm: BFS Shortest Augmenting Paths & O(V E^2) Monotonic Distance Proof

7 min read Module 5.3: Network Flow, Min-Cut & Bipartite Matching Updated Oct 23, 2026
📄 Cheat Sheet
Edmonds-Karp Maximum Flow Algorithm — BFS Shortest Augmenting Paths
William Fiset Edmonds-Karp Maximum Flow Algorithm — BFS Shortest Augmenting Paths Watch on YouTube ↗

Step-by-step trace of BFS augmenting path selection and why shortest-hop paths eliminate capacity-dependent worst cases.

0:00 / --:--

Monotonicity of Residual Distances & The $O(V E)$ Critical Edge Bound

Let $\delta_f(s, v)$ denote the unweighted shortest-path distance (number of edges) from $s$ to $v$ in the residual graph $G_f$. When we augment flow along a shortest path $P$, some forward edges saturate and disappear from $G_f$, while new reverse edges $(v, u)$ appear. Could a newly added reverse edge $(v, u)$ create a shortcut that decreases $\delta(s, w)$?

No! Because $P$ was a shortest path, $(u, v) \in P$ implies $\delta_f(s, v) = \delta_f(s, u) + 1$. Thus traversing $(v, u)$ backward goes from distance $d+1$ to distance $d$, which can never reduce the distance from $s$ to any vertex.

Formal Proof — Edmonds-Karp $O(V E^2)$ Complexity Bound

Lemma 1 (Distance Monotonicity): Under shortest-path augmentations, for every vertex $v \in V$, the residual distance $\delta_{f'}(s, v) \ge \delta_f(s, v)$ never decreases.

Lemma 2 (Critical Edge Saturation Bound): Call an edge $(u, v)$ critical during an augmentation if it is a bottleneck edge ($c_f(u, v) = \Delta$) that becomes saturated and disappears from $G_f$. How many times can $(u, v)$ become critical?

  • When $(u, v)$ is critical, it lies on a shortest path: $\delta_f(s, v) = \delta_f(s, u) + 1$.
  • Before $(u, v)$ can be used again, flow must be pushed along the reverse edge $(v, u)$ at some later flow $f'$, when $\delta_{f'}(s, u) = \delta_{f'}(s, v) + 1$.
  • Combining with Lemma 1 ($\delta_{f'}(s, v) \ge \delta_f(s, v)$): $$\delta_{f'}(s, u) = \delta_{f'}(s, v) + 1 \ge \delta_f(s, v) + 1 = \delta_f(s, u) + 2$$

Every time $(u, v)$ re-enters $G_f$ and becomes critical again, $\delta(s, u)$ increases by at least $2$. Since $0 \le \delta(s, u) \le V - 1$, each of the $2E$ directed arcs is critical at most $V / 2$ times! Thus there are $O(V E)$ augmentations, each taking $O(E)$ BFS time, for $O(V E^2)$ total time. $\blacksquare$

flowchart TD Start["Edmonds-Karp Iteration"] --> BFS["Run unweighted BFS from s in G_f (O(E) time)"] BFS --> Reached{"Did BFS reach t?"} Reached -- "No" --> Terminate["Return totalFlow"] Reached -- "Yes" --> Trace["Trace parentEdge[] backward from t to s to find bottleneck Delta"] Trace --> Update["Subtract Delta on forward edges, add Delta on reverse edges (id ^ 1)"] Update --> Critical["At least one critical edge saturates; delta(s, u) increases by >= 2 before reuse"] Critical --> Start
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 — Why Edmonds-Karp Wastes BFS Work Across Similar Paths

Notice that Edmonds-Karp runs a fresh $O(E)$ BFS from scratch for every single augmenting path. If there are $500$ parallel paths of length $k$, Edmonds-Karp rebuilds the exact same BFS tree $500$ times! This observation directly motivates Dinic's algorithm (next unit), which builds the BFS level graph once per distance level and saturates all paths of length $k$ in a single blocking-flow pass.

MetricBoundExplanation
Total Augmentations$O(V E)$Each of $2E$ residual edges saturates at most $V/2$ times
Cost per Augmentation$O(E)$Unweighted BFS over residual graph $G_f$
Worst-Case Time$O(V E^2)$Strongly polynomial; independent of maximum capacity $U$
Auxiliary Space$O(V + E)$Residual adjacency list + parentEdge[V] BFS array

Production C++20 Implementation: Edmonds-Karp with Predecessor Arc Recording

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

class EdmondsKarp {
public:
    struct Arc {
        int from, to;
        long long cap;
    };

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

    void addEdge(int u, int v, long long cap) {
        adj[u].push_back(static_cast<int>(arcs.size()));
        arcs.push_back({u, v, cap});
        adj[v].push_back(static_cast<int>(arcs.size()));
        arcs.push_back({v, u, 0});
    }

    long long maxFlow(int s, int t) {
        long long flow = 0;
        std::vector<int> parentArc(numVertices, -1);

        while (bfs(s, t, parentArc)) {
            long long bottleneck = std::numeric_limits<long long>::max();
            for (int cur = t; cur != s; ) {
                int id = parentArc[cur];
                bottleneck = std::min(bottleneck, arcs[id].cap);
                cur = arcs[id].from;
            }
            for (int cur = t; cur != s; ) {
                int id = parentArc[cur];
                arcs[id].cap -= bottleneck;
                arcs[id ^ 1].cap += bottleneck;
                cur = arcs[id].from;
            }
            flow += bottleneck;
        }
        return flow;
    }

private:
    int numVertices;
    std::vector<Arc> arcs;
    std::vector<std::vector<int>> adj;

    bool bfs(int s, int t, std::vector<int>& parentArc) const {
        std::fill(parentArc.begin(), parentArc.end(), -1);
        parentArc[s] = -2; // Sentinel for source
        std::queue<int> q;
        q.push(s);

        while (!q.empty()) {
            int u = q.front();
            q.pop();
            for (int id : adj[u]) {
                int v = arcs[id].to;
                if (parentArc[v] == -1 && arcs[id].cap > 0) {
                    parentArc[v] = id;
                    if (v == t) return true;
                    q.push(v);
                }
            }
        }
        return false;
    }
};
Failure Mode — Storing parentVertex[v] = u Instead of parentArc[v] = id

In multigraphs (or networks where both $(u, v)$ and $(v, u)$ exist as original edges), there may be multiple parallel residual arcs between $u$ and $v$. Storing only the parent vertex u forces a linear scan of adj[u] during augmentation to locate the unsaturated arc. Always record the exact arc index parentArc[v] = id during BFS.

Check yourself Question 1 of 4
Concept Check How Edmonds-Karp Selects Augmenting Paths recall
Section: Algorithmic Definition

How does the Edmonds-Karp algorithm choose which augmenting path to push flow along in each iteration?

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

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

How do you find the maximum number of edge-disjoint directed paths from $s$ to $t$ in a directed graph $G = (V, E)$, and what is the runtime using Edmonds-Karp?

Key Takeaways

Edmonds-Karp specializes Ford-Fulkerson by selecting the shortest unweighted s ~> t path in G_f via BFS.
Residual shortest-path distances delta_f(s, v) increase monotonically across augmentations.
Every time an edge (u, v) saturates and later becomes critical again, delta(s, u) increases by at least 2, bounding total augmentations to O(V E) and total time to O(V E^2).
Recording the incoming arc index parentArc[v] = id in BFS supports multigraphs and updates residual capacities in O(1) per path edge.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue