Architecture of the Unified Dinic Flow & Matching Engine
Our production engine combines four essential engineering techniques:
- Paired Arc Storage (
arcs[2k],arcs[2k+1]): EveryaddEdge(u, v, cap)returns the forward arc index2k, allowing callers to inspect the exact final flow on any edge viagetEdgeFlow(edgeId)in $O(1)$ time. - Multi-Branch Blocking Flow DFS: Pushes multiple bottleneck branches from a single DFS invocation using the current-arc reference
int& cid = ptr[u]. - Zero-Cost Min-Cut Extraction from Final
level[]: BecausemaxFlow(s, t)terminates precisely whenbfs(s, t)fails to reach $t$, thelevel[u]array at termination already marks the exact residual reachability set $S = \{u : \text{level}[u] \ne -1\}$! - Bipartite Matching Adapter: Wraps $L \cup R$ into a unit network on $n_L + n_R + 2$ vertices and reads matched pairs directly from saturated $(u, v)$ arcs.
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.
Invariant & State
When we store only residual capacity cap on each Arc (saving 8 bytes per arc by omitting flow), how do we know the net flow $f(e)$ carried by original directed edge id = 2k? Since its reverse residual arc id ^ 1 = 2k + 1 was initialized with capacity $0$, its residual capacity arcs[id ^ 1].cap is identically equal to the current flow $f(e)$ on the forward edge!
By storing only int32_t to, int32_t next, and int64_t cap inside each Arc, every residual arc occupies exactly 16 bytes — fitting 4 arcs per 64-byte L1 cache line. Pre-reserving arcs.reserve(2 * M) upfront eliminates vector reallocations during graph construction and guarantees that forward arc 2k and reverse arc 2k + 1 always reside in the same or adjacent L1 cache line during path augmentation.
| Engine Method | Time Complexity | Memory Footprint |
|---|---|---|
addEdge(u, v, cap) | $O(1)$ amortized | $32\text{ bytes}$ per directed edge pair |
maxFlow(s, t) | $O(V^2 E)$ / $O(E \sqrt{V})$ unit | Reuses pre-allocated level, ptr, q |
getMinCutEdges() | $O(E)$ | Reads level[u] != -1 without an extra BFS |
getEdgeFlow(edgeId) | $O(1)$ | Returns arcs[edgeId ^ 1].cap |
Complete Production C++20 Implementation: Unified Dinic Flow, Min-Cut & Matching Engine
#include <vector>
#include <queue>
#include <algorithm>
#include <limits>
class DinicEngine {
public:
struct Arc {
int from, to;
long long cap;
};
explicit DinicEngine(int n) : n(n), adj(n), level(n, -1), ptr(n, 0) {}
// Returns the forward arc index (even integer 2k)
int addEdge(int u, int v, long long cap) {
int id = static_cast<int>(arcs.size());
adj[u].push_back(id);
arcs.push_back({u, v, cap});
adj[v].push_back(id + 1);
arcs.push_back({v, u, 0});
return id;
}
long long maxFlow(int s, int t) {
long long totalFlow = 0;
while (bfs(s, t)) {
std::fill(ptr.begin(), ptr.end(), 0);
while (long long pushed = dfs(s, t, std::numeric_limits<long long>::max())) {
totalFlow += pushed;
}
}
return totalFlow;
}
// Exact flow pushed along the edge returned by addEdge()
long long getEdgeFlow(int edgeId) const noexcept {
return arcs[edgeId ^ 1].cap;
}
// True iff vertex u belongs to the source side S of the minimum s-t cut
bool inSourceSide(int u) const noexcept {
return level[u] != -1;
}
std::vector<std::pair<int, int>> extractMinCutEdges() const {
std::vector<std::pair<int, int>> cut;
for (size_t id = 0; id < arcs.size(); id += 2) {
const auto& e = arcs[id];
if (level[e.from] != -1 && level[e.to] == -1 && (e.cap + arcs[id ^ 1].cap) > 0) {
cut.emplace_back(e.from, e.to);
}
}
return cut;
}
private:
int n;
std::vector<Arc> arcs;
std::vector<std::vector<int>> adj;
std::vector<int> level, ptr;
bool bfs(int s, int t) {
std::fill(level.begin(), level.end(), -1);
level[s] = 0;
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 (arcs[id].cap > 0 && level[v] == -1) {
level[v] = level[u] + 1;
q.push(v);
}
}
}
return level[t] != -1;
}
long long dfs(int u, int t, long long available) {
if (available == 0 || u == t) return available;
long long totalPushed = 0;
for (int& cid = ptr[u]; cid < static_cast<int>(adj[u].size()); ++cid) {
int id = adj[u][cid];
int v = arcs[id].to;
if (level[v] != level[u] + 1 || arcs[id].cap == 0) continue;
long long tr = dfs(v, t, std::min(available - totalPushed, arcs[id].cap));
if (tr == 0) continue;
arcs[id].cap -= tr;
arcs[id ^ 1].cap += tr;
totalPushed += tr;
if (totalPushed == available) return totalPushed;
}
return totalPushed;
}
};
Some Dinic implementations optimize bfs(s, t) by returning true immediately when $t$ is pushed into the queue. While valid during intermediate phases, on the final unsuccessful BFS $t$ is never reached anyway—so level[u] != -1 always holds the exact complete source partition $S$ without needing an extra BFS pass!