Dinic 最大流算法

代码模板

Dinic 最大流算法的原理与实现:分层图、多路增广、当前弧优化。

一句话算法

Dinic 先用 BFS 把残量网络分层,再用 DFS 在分层图里一次推完尽可能多的流。

问题模型

给定一个有向网络:

  • 每条边有容量;
  • 源点 s 产生流量;
  • 汇点 t 接收流量;
  • 除 s 和 t 外,每个点满足流量守恒。

目标是求从 s 到 t 的最大流。

Dinic 和 Edmonds-Karp 解决的是同一个最大流问题,但 Dinic 不再每次只找一条增广路,而是在一张分层图里批量增广。

核心直觉

Edmonds-Karp 每轮 BFS 找一条最短增广路。问题是:一条一条找太慢。

Dinic 的改进是:

  1. BFS 得到每个点距离源点的层数。
  2. 只允许流从第 k 层流向第 k+1 层。
  3. 在这张分层图上 DFS,把所有能推的最短增广路尽量推完。
  4. 当前分层图推不动后,重新 BFS 分层。

分层图像一个方向明确的水渠系统,DFS 只沿着层数增加的边走,减少无效搜索。

关键概念

残量网络

一条边剩余还能通过多少流量,叫残量容量。

每加入一条正向边,都要加入一条反向边。正向边被使用后,反向边容量增加,表示后续可以撤销这部分流量。

分层图

BFS 从源点出发,只走残量容量大于 0 的边。

若 level[v] = level[u] + 1,边 u -> v 就是当前分层图中的允许边。

当前弧优化

DFS 从点 u 尝试过一条边后,如果这条边已经没有可用价值,同一轮分层图里没必要再从头扫描。

所以每个点维护 current[u],记录下一条应该尝试的边。

算法步骤

  1. 建图时,每条原边加入一条容量为 0 的反向边。
  2. 在残量网络上 BFS:
    • 若汇点不可达,算法结束;
    • 否则得到分层图。
  3. 将当前弧数组 current 初始化为 head。
  4. 从源点 DFS 增广:
    • 只走 capacity > 0 且 level[v] = level[u] + 1 的边;
    • 找到流量后修改正向边和反向边;
    • 一轮 BFS 下反复 DFS,直到源点再也推不出流。
  5. 重新 BFS,继续下一轮。

算法证明

核心不变量:残量网络始终正确表示当前流量还能如何增加或撤销。

每次 DFS 沿允许边增广时:

  1. 正向边容量减少,表示使用了容量。
  2. 反向边容量增加,表示这部分流量允许被撤销。
  3. 流量仍满足容量限制和流量守恒。

BFS 分层只保留当前残量网络中的最短增广方向。DFS 把当前分层图中的可行增广尽量推完后,当前最短长度的增广路已经不存在。

当某次 BFS 无法到达汇点时,残量网络中不存在任何 s -> t 路径。此时源点可达集合与不可达集合之间没有剩余容量,形成一个割。根据最大流最小割定理,当前流就是最大流。

复杂度分析

设点数为 VV,边数为 EE。

  • 一次 BFS:O(E)O(E)。
  • 一轮分层图中的 DFS 在当前弧优化下会顺序扫描边。
  • Dinic 一般图最坏时间复杂度:O(V2E)O(V^2E)。
  • 二分图匹配等特殊网络上有更好的复杂度表现。
  • 空间复杂度:O(V+E)O(V+E)。

实际竞赛中,Dinic 是最常用的最大流模板之一,通常比 Edmonds-Karp 快很多。

代码实现

模板输入格式:

n m s t
u1 v1 c1
u2 v2 c2
...
um vm cm

输出最大流。

C++
cpp
        
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
#include <bits/stdc++.h> using namespace std; struct Dinic { struct Edge { int to; int next; long long capacity; }; vector<Edge> edges; vector<int> head; vector<int> level; vector<int> current; int node_count = 0; Dinic(int n = 0, int max_edges = 0) { init(n, max_edges); } void init(int n, int max_edges = 0) { node_count = n; edges.clear(); edges.reserve(max_edges * 2 + 5); head.assign(n + 1, -1); level.resize(n + 1); current.resize(n + 1); } void add_edge(int u, int v, long long capacity) { edges.push_back({v, head[u], capacity}); head[u] = (int)edges.size() - 1; edges.push_back({u, head[v], 0}); head[v] = (int)edges.size() - 1; } bool bfs(int source, int sink) { fill(level.begin(), level.end(), -1); queue<int> q; level[source] = 0; q.push(source); while (!q.empty()) { int u = q.front(); q.pop(); for (int i = head[u]; i != -1; i = edges[i].next) { int v = edges[i].to; if (edges[i].capacity <= 0 || level[v] != -1) continue; level[v] = level[u] + 1; q.push(v); } } return level[sink] != -1; } long long dfs(int u, int sink, long long limit) { if (u == sink || limit == 0) return limit; long long flow = 0; for (int &i = current[u]; i != -1; i = edges[i].next) { int v = edges[i].to; if (edges[i].capacity <= 0 || level[v] != level[u] + 1) continue; long long pushed = dfs(v, sink, min(limit, edges[i].capacity)); if (pushed == 0) continue; edges[i].capacity -= pushed; edges[i ^ 1].capacity += pushed; flow += pushed; limit -= pushed; if (limit == 0) break; } if (flow == 0) level[u] = -1; return flow; } long long max_flow(int source, int sink) { long long answer = 0; while (bfs(source, sink)) { current = head; while (true) { long long flow = dfs(source, sink, numeric_limits<long long>::max()); if (flow == 0) break; answer += flow; } } return answer; } }; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, m, source, sink; cin >> n >> m >> source >> sink; Dinic dinic(n, m); for (int i = 0; i < m; i++) { int u, v; long long capacity; cin >> u >> v >> capacity; dinic.add_edge(u, v, capacity); } cout << dinic.max_flow(source, sink) << '\n'; return 0; }
Python
python
        
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# Dinic 最大流:add_edge 同时加正向边(容量 cap)和反向边(容量 0),编号成对,i ^ 1 是配对边。 # 点编号 1..n(下标 0 不用);capacity 是 long long,在 Python 里是任意精度 int,不会溢出。 # max_flow 前必须 add_edge 建好图;dfs 的 limit 用 LLONG_MAX 复刻 C++ 的 numeric_limits<long long>::max()。 # dfs 递归深度最坏 n(1e5+),使用前需 sys.setrecursionlimit(1 << 20)。 LLONG_MAX = (1 << 63) - 1 # C++ long long 的最大值,作为「不限流量」的初值 class Edge: """残留网络的一条弧:to 终点、next 同起点的下一条弧、capacity 剩余容量。""" __slots__ = ("to", "next", "capacity") def __init__(self, to: int, next: int, capacity: int) -> None: self.to = to self.next = next self.capacity = capacity class Dinic: """head 是每个点的第一条弧;current 是当前弧优化的游标;level 是 BFS 分层。""" def __init__(self, n: int = 0, max_edges: int = 0) -> None: self.init(n, max_edges) def init(self, n: int, max_edges: int = 0) -> None: self.node_count = n self.edges: list[Edge] = [] # max_edges 只对应 C++ 的 reserve,Python 列表动态扩容,保留参数仅为接口对齐。 self.head = [-1] * (n + 1) self.level = [0] * (n + 1) self.current = [0] * (n + 1) def add_edge(self, u: int, v: int, capacity: int) -> None: self.edges.append(Edge(v, self.head[u], capacity)) self.head[u] = len(self.edges) - 1 self.edges.append(Edge(u, self.head[v], 0)) # 反向边,初始容量 0 self.head[v] = len(self.edges) - 1 def bfs(self, source: int, sink: int) -> bool: """按残留容量做分层;返回 sink 是否可达。""" self.level = [-1] * (self.node_count + 1) self.level[source] = 0 q = [source] head = 0 while head < len(q): u = q[head] head += 1 i = self.head[u] while i != -1: v = self.edges[i].to if self.edges[i].capacity > 0 and self.level[v] == -1: self.level[v] = self.level[u] + 1 q.append(v) i = self.edges[i].next return self.level[sink] != -1 def dfs(self, u: int, sink: int, limit: int) -> int: if u == sink or limit == 0: return limit flow = 0 i = self.current[u] while i != -1: edge = self.edges[i] v = edge.to if edge.capacity <= 0 or self.level[v] != self.level[u] + 1: i = edge.next continue pushed = self.dfs(v, sink, min(limit, edge.capacity)) if pushed == 0: i = edge.next continue edge.capacity -= pushed self.edges[i ^ 1].capacity += pushed flow += pushed limit -= pushed if limit == 0: break i = edge.next # 回写当前弧(对应 C++ 的 int& i = current[u]);本层已无增广路则把 u 从分层图删掉。 self.current[u] = i if flow == 0: self.level[u] = -1 return flow def max_flow(self, source: int, sink: int) -> int: answer = 0 while self.bfs(source, sink): self.current = self.head[:] # current = head,每次重新从各点第一条弧开始 while True: flow = self.dfs(source, sink, LLONG_MAX) if flow == 0: break answer += flow return answer

测试用例

输入:

4 5 1 4
1 2 3
1 3 2
2 3 1
2 4 2
3 4 4

输出:

5

这组数据中,源点 1 的总出边容量为 5,并且这些流量都能到达汇点 4,所以最大流为 5。

应用分类详解

Dinic 的本质是高效求最大流。只要题目能被建成“容量网络”,就可以考虑 Dinic。

一、最大流模板与运输问题

典型模式: 有源点、汇点、通道容量,问最多能输送多少。

识别信号: 出现“容量”“流量”“最多运输”“管道”“网络”。

核心建模: 点表示位置或状态,边容量表示最大可通过量。

应用场景 经典题目 核心思路
最大流模板 luogu-P3376 直接按容量建图
排水沟模型 poj-1273 管道容量就是边容量

二、二分图最大匹配

典型模式: 左右两类对象配对,每个对象最多匹配一次。

识别信号: 出现“匹配”“分配”“每个只能选一个”。

核心建模: S -> 左部 容量为 1,可匹配关系连 左部 -> 右部,右部 -> T 容量为 1。

应用场景 经典题目 核心思路
二分图匹配 luogu-P3386 单位容量网络最大流
食物与饮料分配 poj-3281 拆点限制每头牛只能被选一次

三、带点容量限制的问题

典型模式: 不只是边有容量,点也有通过次数限制。

识别信号: 出现“每个点最多使用一次”“每个人最多承担一个任务”。

核心建模: 把点 x 拆成 x_in -> x_out,这条边容量就是点容量。

四、最小割模型

典型模式: 要在若干代价中选择切断一些关系,使两个集合分开。

识别信号: 出现“最小代价切断”“二选一收益/损失”“闭合子图”。

核心建模: 最大流等于最小割,求完最大流后从残量网络可达性可以恢复割。

经典例题

1. luogu-P3376

最大流模板题。重点练习反向边、分层图和当前弧。

2. luogu-P3386

二分图最大匹配。可以用 Dinic 建单位容量网络,也可以使用匈牙利算法。

3. poj-3281

Dining。经典拆点建图题:食物到牛,牛拆点后到饮料,保证每头牛只被选一次。

参考

  • 本书 Edmonds-Karp 章节:graph/netflow/ek.md