Network flow¶
Use maximum flow when units move from a source to a sink through directed edges with capacities. The answer is the largest feasible amount, not the cheapest path.
Model before algorithm¶
For every directed edge u → v:
0 ≤ flow(u,v) ≤ capacity(u,v);- every internal vertex has equal incoming and outgoing flow;
- the objective is total flow leaving the source, equivalently entering the sink.
If a problem says “assign as many,” “route as much,” or “match the maximum number” under pairwise capacity constraints, try a flow model.
The residual graph¶
The residual graph records what can still change:
- a forward residual edge allows more flow to be sent;
- a reverse residual edge allows earlier flow to be cancelled and rerouted.
Reverse edges are not optional bookkeeping. Without them, an early valid choice can permanently block a better global solution.
flowchart LR
accTitle: A residual edge supports both augmentation and cancellation
accDescr: Sending delta units from u to v decreases the forward residual capacity and increases the reverse residual capacity, allowing later augmenting paths to cancel or reroute that flow.
U["u"] -->|"forward residual = capacity - flow"| V["v"]
V -.->|"reverse residual = flow"| U
P["Augment by delta"] --> F["Forward residual<br/>subtract delta"]
P --> R["Reverse residual<br/>add delta"]
F --> Q["More capacity used"]
R --> Z["Same flow can be cancelled<br/>and rerouted later"]
Think of the reverse edge as an undo budget. Every unit already sent forward creates one unit of permission to revise that choice.
Annotated Dinic blueprint¶
add_edge(u, v, capacity):
add forward residual edge with capacity
add reverse residual edge with zero capacity # enables cancellation
while BFS can reach sink:
level[v] = shortest residual-edge distance from source
next_edge[v] = 0 # avoid rescanning dead edges
while pushed = DFS(source, infinity):
total_flow += pushed
DFS(u, available):
if u is sink: return available
inspect only edges with level[v] = level[u] + 1
push min(available, residual_capacity)
subtract pushed flow from forward edge
add pushed flow to reverse edge # preserve residual invariant
Invariant: every residual update preserves capacity constraints and flow conservation.
Algorithm choice¶
| Algorithm | Useful when | Typical bound |
|---|---|---|
| Edmonds–Karp | learning, small graphs, simplest correctness story | O(VE²) |
| Dinic | general interview and contest flow | O(V²E) general worst case |
| Hopcroft–Karp | the model is specifically unweighted bipartite matching | O(E√V) |
Common reductions¶
- Bipartite matching: source → left side → right side → sink, all capacity one.
- Vertex capacity: split
vintov_in → v_outwith that capacity. - Choose at most
k: place capacitykon the controlling edge. - Disjoint paths: capacity one on each edge or split vertices for vertex-disjoint paths.
- Minimum cut: after max flow, vertices still reachable in the residual graph form the source side of a minimum cut.
Failure checks¶
- Are capacities integral and within the numeric type?
- Did every added edge receive a paired reverse edge?
- Are parallel edges accumulated or represented separately?
- Is the graph directed as modeled, even when the story sounds symmetric?
- Does the reduction preserve “at most one” constraints on both sides?
Reference¶
Tested Dinic template¶
@dataclass
class _FlowEdge:
to: int
reverse: int
capacity: int
class Dinic:
"""Compute maximum flow with level graphs and blocking-flow DFS."""
def __init__(self, vertex_count: int) -> None:
if vertex_count < 0:
raise ValueError("vertex_count must be nonnegative")
self.graph: list[list[_FlowEdge]] = [[] for _ in range(vertex_count)]
def add_edge(self, left: int, right: int, capacity: int) -> None:
_validate_vertex(left, len(self.graph), "edge endpoint")
_validate_vertex(right, len(self.graph), "edge endpoint")
if capacity < 0:
raise ValueError("capacity must be nonnegative")
forward = _FlowEdge(right, len(self.graph[right]), capacity)
backward = _FlowEdge(left, len(self.graph[left]), 0)
self.graph[left].append(forward)
self.graph[right].append(backward)
def max_flow(self, source: int, sink: int) -> int:
_validate_vertex(source, len(self.graph), "source")
_validate_vertex(sink, len(self.graph), "sink")
if source == sink:
return 0
total = 0
while True:
level = [-1] * len(self.graph)
level[source] = 0
queue = deque([source])
while queue:
node = queue.popleft()
for edge in self.graph[node]:
if edge.capacity > 0 and level[edge.to] == -1:
level[edge.to] = level[node] + 1
queue.append(edge.to)
if level[sink] == -1:
return total
next_edge = [0] * len(self.graph)
def push(node: int, available: int) -> int:
if node == sink:
return available
while next_edge[node] < len(self.graph[node]):
edge = self.graph[node][next_edge[node]]
if edge.capacity > 0 and level[edge.to] == level[node] + 1:
sent = push(edge.to, min(available, edge.capacity))
if sent:
# Forward and reverse residual capacities change together.
edge.capacity -= sent
self.graph[edge.to][edge.reverse].capacity += sent
return sent
next_edge[node] += 1
return 0
while True:
sent = push(source, 10**30)
if not sent:
break
total += sent
class Dinic {
public:
explicit Dinic(int vertex_count) {
if (vertex_count < 0) {
throw std::invalid_argument("vertex_count must be nonnegative");
}
graph_.resize(static_cast<std::size_t>(vertex_count));
}
void add_edge(int left, int right, long long capacity) {
validate_vertex(left, graph_.size(), "edge endpoint");
validate_vertex(right, graph_.size(), "edge endpoint");
if (capacity < 0) {
throw std::invalid_argument("capacity must be nonnegative");
}
const int left_reverse = static_cast<int>(graph_[right].size());
const int right_reverse = static_cast<int>(graph_[left].size());
graph_[left].push_back(Edge(right, left_reverse, capacity));
graph_[right].push_back(Edge(left, right_reverse, 0));
}
long long max_flow(int source, int sink) {
validate_vertex(source, graph_.size(), "source");
validate_vertex(sink, graph_.size(), "sink");
if (source == sink) {
return 0;
}
long long total = 0;
while (build_levels(source, sink)) {
next_edge_.assign(graph_.size(), 0);
while (true) {
const long long sent =
push(source, sink, std::numeric_limits<long long>::max());
if (sent == 0) {
break;
}
if (total > std::numeric_limits<long long>::max() - sent) {
throw std::overflow_error("maximum flow overflow");
}
total += sent;
}
}
return total;
}
private:
struct Edge {
Edge(int edge_to, int reverse_index, long long residual_capacity)
: to(edge_to), reverse(reverse_index), capacity(residual_capacity) {}
int to;
int reverse;
long long capacity;
};
bool build_levels(int source, int sink) {
level_.assign(graph_.size(), -1);
level_[source] = 0;
std::queue<int> queue;
queue.push(source);
while (!queue.empty()) {
const int node = queue.front();
queue.pop();
for (const auto& edge : graph_[node]) {
if (edge.capacity > 0 && level_[edge.to] == -1) {
level_[edge.to] = level_[node] + 1;
queue.push(edge.to);
}
}
}
return level_[sink] != -1;
}
long long push(int node, int sink, long long available) {
if (node == sink) {
return available;
}
for (int& index = next_edge_[node];
index < static_cast<int>(graph_[node].size()); ++index) {
Edge& edge = graph_[node][index];
if (edge.capacity == 0 || level_[edge.to] != level_[node] + 1) {
continue;
}
const long long sent = push(edge.to, sink, std::min(available, edge.capacity));
if (sent > 0) {
// Forward and reverse residual capacities change together.
edge.capacity -= sent;
graph_[edge.to][edge.reverse].capacity += sent;
return sent;
}
}
return 0;
}
std::vector<std::vector<Edge>> graph_;
std::vector<int> level_;
std::vector<int> next_edge_;
};
class Dinic {
public:
explicit Dinic(int vertex_count) {
if (vertex_count < 0) {
throw std::invalid_argument("vertex_count must be nonnegative");
}
graph_.resize(static_cast<std::size_t>(vertex_count));
}
void add_edge(int left, int right, long long capacity) {
validate_vertex(left, graph_.size(), "edge endpoint");
validate_vertex(right, graph_.size(), "edge endpoint");
if (capacity < 0) {
throw std::invalid_argument("capacity must be nonnegative");
}
const int left_reverse = static_cast<int>(graph_[right].size());
const int right_reverse = static_cast<int>(graph_[left].size());
graph_[left].push_back(Edge(right, left_reverse, capacity));
graph_[right].push_back(Edge(left, right_reverse, 0));
}
long long max_flow(int source, int sink) {
validate_vertex(source, graph_.size(), "source");
validate_vertex(sink, graph_.size(), "sink");
if (source == sink) {
return 0;
}
long long total = 0;
while (build_levels(source, sink)) {
next_edge_.assign(graph_.size(), 0);
while (true) {
const long long sent =
push(source, sink, std::numeric_limits<long long>::max());
if (sent == 0) {
break;
}
if (total > std::numeric_limits<long long>::max() - sent) {
throw std::overflow_error("maximum flow overflow");
}
total += sent;
}
}
return total;
}
private:
struct Edge {
Edge(int edge_to, int reverse_index, long long residual_capacity)
: to(edge_to), reverse(reverse_index), capacity(residual_capacity) {}
int to;
int reverse;
long long capacity;
};
bool build_levels(int source, int sink) {
level_.assign(graph_.size(), -1);
level_[source] = 0;
std::queue<int> queue;
queue.push(source);
while (!queue.empty()) {
const int node = queue.front();
queue.pop();
for (const auto& edge : graph_[node]) {
if (edge.capacity > 0 && level_[edge.to] == -1) {
level_[edge.to] = level_[node] + 1;
queue.push(edge.to);
}
}
}
return level_[sink] != -1;
}
long long push(int node, int sink, long long available) {
if (node == sink) {
return available;
}
for (int& index = next_edge_[node];
index < static_cast<int>(graph_[node].size()); ++index) {
Edge& edge = graph_[node][index];
if (edge.capacity == 0 || level_[edge.to] != level_[node] + 1) {
continue;
}
const long long sent = push(edge.to, sink, std::min(available, edge.capacity));
if (sent > 0) {
// Forward and reverse residual capacities change together.
edge.capacity -= sent;
graph_[edge.to][edge.reverse].capacity += sent;
return sent;
}
}
return 0;
}
std::vector<std::vector<Edge>> graph_;
std::vector<int> level_;
std::vector<int> next_edge_;
};
The implementation keeps reverse-edge indices beside every residual edge. Each blocking-flow phase scans only edges that advance one level.