最大流:EK、Dinic 算法
最大流:EK、Dinic 算法
首先做出一些定义,给定一个图 G(V, E)
,这里 V
时图上顶点集合,E
是有向边的集合,在实际应用中,E 中任意一条边都包含:头,尾,容量 cap,流量 flow,4 个属性。
- cap 指定了当前边能够通过的最大流量(定值)。
- flow 则记录了当前边通过的流量(初始为 0,表示没有流量通过)。
另外会定义两个特殊的点,源点 s 和 汇点 t,分别代表网络流的流入节点和流出节点。
可行流 满足:
- 源点 s:流出量 = 整个网络的流量。
- 汇点 t:流入量 = 整个网络的流量。
- 中间点:总流入量 = 总流出量。
例如:

红色部分表示流量 flow,黑色部分表示容量 cap。可行流为 7。
最大流:
- 所有可行流中流量最大的流量。
- 最大流可能不止一个。
反向边:
首先要知道,在一条从源点 s 到汇点 t 的路径中,能够带来的最大流量取决于这条路径上的最小容量。

如上所示,如果我们使用搜索算法找到一条从 S 到 T 的路径,并且这条路径能够带来的流量就是这条路径上边的容量 cap 的最小值。
假设找到的路径是
这个时候,为了能够继续找到路径,必须采用反向建边,把当前这条找到的路径进行反向建边,边的权值就是这条路径的流量 flow。如下所示:

其中红色的边为反向边。这时继续寻找路径,发现可以走
为什么要反向建边
仔细想想应该能够明白,反向建边的作用相当于让之前的路径可以有反悔的余地。这样即使一开始走错也没关系,因为可以通过反向边来反悔,使得最终一定能够得到正确的 ans。
增广路
最大流的核心在于寻找增广路。
如果一个可行流不是最大流,那么当前网络中一定还存在增广路径。
从源点 S 到汇点 T 的一条路径中,如果边
- 正向边
。 - 逆向边
。
则该路径是增广路径。即增广路径是这样一条路径,可以用来增加到达汇点的流量,并且路径中的流量没有超过每条边的 cap。
沿这条增广路改进可行流的操作称为增广,而所有可能的增广路径放在一起就构成了残余网络。
以下两个算法,都是基于不断找增广路径来实现的。
EK 算法
时间复杂度:
首先考虑的是怎么找增广路径,先前说用搜索算法,可以用 bfs (dfs)。但是 bfs 的好处在于能够在残余网络中每一次找到最短的一条增广路径。因此EK算法是基于 bfs 来找增广路径的,bfs 每执行一次,就找出一条增广路,然后把这条路径上的权值进行修改,同时反向建边。直到找不到增广路径为止。
过程:
-
采用邻接矩阵存储图。(通常情况下,都是稠密图,故用邻接矩阵存储)
-
利用 bfs 每次找到一条最短增广路径,记录当前该路径的最小流量 inc。
-
更新邻接矩阵这条路径上的流量。
-
不断重复 2,3 直到没有增广为止。
模板题:P2740 [USACO4.2] 草地排水Drainage Ditches
def solve(): m, n = map(int, readline().split()) adj = [[0] * (n + 1) for _ in range(n + 1)] for i in range(m): x, y, c = map(int, readline().split()) adj[x][y] += c pre = defaultdict(lambda: int(-1)) def bfs(u: int) -> int: nonlocal pre pre = defaultdict(lambda: int(-1)) dist = [0 for _ in range(n + 1)] dist[u] = float('inf') q = deque() q.append(s) while q: x = q.popleft() if x == t: break # 找到了汇点 for y in range(1, n + 1): if pre[y] == -1 and adj[x][y] > 0: # 找到一个没有访问,且还有 cap 的点 pre[y] = x dist[y] = min(dist[x], adj[x][y]) # 更新增广路的最小流量 q.append(y) return (dist[t] if pre[t] != -1 else -1) def EK() -> int: res = 0 while True: inc = bfs(s) if inc == -1: break y = t while y != s: x = pre[y] adj[x][y] -= inc # 正向边容量减去流量 adj[y][x] += inc # 逆向边建立 y = pre[y] res += inc return res s, t = 1, n ans = EK() print(ans)
Dinic算法
时间复杂度:
EK 算法找增广路径是基于 bfs 来进行的, bfs 会把周围能够扩展的点全部扩展进来,直到找到汇点为止,相当于每找一次增广路径都要搜索大量的点。 Dinic 算法实际上是对 EK 的优化。
Dinic 算法利用 bfs 建立分层网络(按照每个点到源点的距离进行分层)。然后基于这个分层网络,利用 dfs 找到当前分层网络下的所有增广路径,并做好相应的修改。之后不断重复这两个过程,直到无法分层为止,这样做只需要一次 bfs 就可以找到一簇增广路径。
所谓分层网络,就是利用 bfs 特性,以源点为起点,一直向外扩散。每经过一个点都打一个标记,标记到源点的路径所经过的最少的边的数量,假设用
dist[i]
表示 i 到源点 s 的所经过的边的数量,这样就可以将整个网络分层。基于这个分层网络,dfs 在找增广路时,就可以找到最短的增广路径。如果当前点是 x,dfs 搜到下一个点 y,一定要满足 dist[y] = dist[x] + 1,这样才是最短的增广路径,效率才是最高的。
过程:
-
采用邻接矩阵存储图。
-
利用 bfs 建立分层网络(记录每个节点的深度)。
-
按照当前分层网络进行 dfs (一层一层找),找到所有该分层网络下的增广路径,并更新残余网络)。
-
重复 1,2 直到无法建立分层网络为止。
def solve(): m, n = map(int, readline().split()) adj = [[0] * (n + 1) for _ in range(n + 1)] for i in range(m): x, y, c = map(int, readline().split()) adj[x][y] += c dep = [float('inf') for _ in range(n + 1)] def bfs() -> int: nonlocal dep dep = [float('inf') for _ in range(n + 1)] dep[s] = 0 q = deque() q.append(s) while q: x = q.popleft() for y in range(1, n + 1): if adj[x][y] > 0 and dep[y] > dep[x] + 1: dep[y] = dep[x] + 1 q.append(y) return (True if dep[t] < float('inf') else False) def dfs(x: int, mn: int) -> int: inc = 0 if x == t: return mn for y in range(1, n + 1): if adj[x][y] > 0 and dep[y] == dep[x] + 1: inc = dfs(y, min(mn, adj[x][y])) if inc > 0: adj[x][y] -= inc adj[y][x] += inc return inc return 0 def dinic() -> int: res = 0 while bfs(): res += dfs(s, inf) return res s, t = 1, n ans = dinic() print(ans)
【References】
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?