想象一座城市的水务调度中心:水库是唯一水源(记为 S),城市是唯一目的地(记为 T),中间是一张由粗细不一的管道组成的供水网。每条管道每分钟能通过的最大水量,就是这条管道的“容量”。水务工程师想知道:这套管网每分钟最多能送多少水?这就是最大流问题。与此同时,入侵者想知道:最少切断哪几根管道,就能让城市彻底断水?切断管道的容量总和就是一个“割”的容量。最大流与最小割看起来是攻守两方的问题,却像一枚硬币的两面——最大流的值恰好等于最小割的容量。本文将从供水网络的建模出发,沿着“找路、优化、分层”的思路,把 Ford-Fulkerson、Edmonds-Karp、Dinic 算法逐一讲透,最后证明这个攻守平衡定理,并看看它如何解决面试和 AI 工程中的真实问题。

岗位关联

掌握最大流与最小割,你就多了一把能同时解决“资源调度”和“最优切割”两大难题的通用建模钥匙,这是算法工程师、推荐系统和图像处理从业者必须随身携带的底层武器。

目录
  1. 可视化演示
  2. 网络流模型
  3. 最大流问题
  4. Ford-Fulkerson 方法
  5. 最短增广路(Edmonds-Karp)
  6. Dinic 算法
  7. 最大流最小割定理
  8. 经典应用
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

本节配有交互动效(进入视口后自动加载,约 3KB)。

网络流模型

想象水务工程师画一张图:水库是起点,城市是终点,中间的加压站和分岔口都是节点,节点之间用单向管道连接。每条管道上都标着一个数字——它的粗细,也就是每分钟最多能通过多少立方米水。这个带容量标注的有向图,就是流网络的直观形象:它不关心管道里现在有多少水,只关心“最多能放多少水进去”的规则。

计算机里的流网络可以写成严谨的形式:一个流网络是带权有向图 $G=(V,E)$,每条边 $(u,v)$ 有一个非负容量 $c(u,v)$。我们把源点记为 $s$,汇点记为 $t$。定义在边上的流 $f(u,v)$ 描述当前的送水量,它必须遵守两条“水规”:第一,任何管道里的水量不能超过它的管径;第二,除了水库和城市,每个中转站既不能凭空产生水,也不能偷偷存水,流入量必须等于流出量。

理解流网络,就像先看懂供水管网图,你才谈得上调度和防断水。有了这两个约束,任意一组满足条件的 $f$ 就是一个“可行流”,下一节我们就要从中寻找那个最大的送水量。

0≤f(u,v)≤c(u,v),∑(u,v)∈Ef(u,v)=∑(v,w)∈Ef(v,w)(v≠s,t)\displaystyle 0 \le f(u,v) \le c(u,v), \quad \sum_{(u,v)\in E} f(u,v) = \sum_{(v,w)\in E} f(v,w) \quad (v \ne s, t)
这个公式是网络流世界的“物理定律”:容量限制防止管道爆裂,流量守恒保证水不增不减。
  1. 容量限制每条管道的水流不能为负,也不能超过管道粗细。
    0≤f(u,v)≤c(u,v)\displaystyle 0 \le f(u,v) \le c(u,v)
  2. 流量守恒对任意非源非汇的中间节点,进水量等于出水量。
    ∑(u,v)∈Ef(u,v)=∑(v,w)∈Ef(v,w)\displaystyle \sum_{(u,v)\in E} f(u,v) = \sum_{(v,w)\in E} f(v,w)

最大流问题

水务工程师的问题很直接:在不动工改管道的前提下,这套管网每分钟最多能送多少水到城市?这个值叫最大流,记作 $|f|$。因为流量守恒,从源点流出的总水量,一定等于流入汇点的总水量,所以最大流可以定义为源点出流量的最大值。

怎么找最大流?一个朴素想法是:在管网里找一条还能“挤水”的路线。一条从 $s$ 到 $t$ 的路线只要每条管道都还有剩余空间,就可以往上加水量。能加多少,取决于这条路线上最细的那根管道,这个“最细处”叫瓶颈。我们把这样的路线称为增广路径,它的瓶颈容量是:

最大流问题像在问“全城供水极限”,而增广路径像一条条还能加水的备用管线路。算法要做的事,就是反复找出这些备用路线,直到整张管网彻底“挤不出”更多水为止。

∣f∣=∑(s,v)∈Ef(s,v)=∑(v,t)∈Ef(v,t)\displaystyle |f| = \sum_{(s,v)\in E} f(s,v) = \sum_{(v,t)\in E} f(v,t)
瓶颈容量决定了这条备用路线一次能送多少水,是所有剩余容量中的最小值。
  1. 源点总出流量把水库连接到各条管道的水量全部加起来。
    ∑(s,v)∈Ef(s,v)\displaystyle \sum_{(s,v)\in E} f(s,v)
  2. 汇点总入流量城市收到的水必然等于水库放出的水。
    cf(P)=min⁡(u,v)∈Pcf(u,v)\displaystyle c_f(P) = \min_{(u,v)\in P} c_f(u,v)

Ford-Fulkerson 方法

水管工有个朴素的笨办法:只要还能找到一条从水库到城市、且每段都有余量的路,就把这条路线的瓶颈水量灌进去,然后重新看管网。这个反复“找路、灌水”的过程,就是 Ford-Fulkerson 方法的核心思想。它的关键在于,每次灌完水后不能只看原管道,还要看“剩余网络”。

为什么需要残余网络?因为水流进管道后,之前的选择可能不是最优的。为了允许“后悔”,我们要在残余网络中给每条已用管道加一条反向边,反向边的容量等于已经灌进去的水量。这样,如果后来发现某条路更优,就可以沿反向边把之前的水退回来,再绕到更好的路线上。残余容量定义为:

每找到一条增广路径 $P$,就沿着它更新路径上所有边的流量,增量就是瓶颈容量 $c_f(P)$。Ford-Fulkerson 的复杂度是 $O(E |f^*|)$,其中 $|f^*|$ 是最大流的值。如果最大流很大或容量是奇怪的数字,它可能跑得非常慢,甚至不终止。水管工很快意识到:不能乱找路线,得讲究策略。

cf(u,v)=c(u,v)−f(u,v)+f(v,u)\displaystyle c_f(u,v) = c(u,v) - f(u,v) + f(v,u)
残余容量 = 原管道剩余空间 + 隔壁反向管道可以退回的水量,它刻画了“还能改多少”。
  1. 原管道剩余当前流量把管径占掉了一部分,剩余空间是 $c(u,v)-f(u,v)$。
    c(u,v)−f(u,v)\displaystyle c(u,v) - f(u,v)
  2. 反向退回空间如果之前从 $v$ 到 $u$ 送过水,就可以退回这部分,所以加上 $f(v,u)$。
    + f(v,u)\displaystyle +\, f(v,u)

最短增广路(Edmonds-Karp)

水管工决定每次只走“跳数最少”的备用路线,也就是从水库到城市经过的管道条数最少。这个策略就是 Edmonds-Karp 算法:每次用 BFS 在残余网络中找最短增广路,然后增广。BFS 像一层一层向外扩散的波纹,先碰到城市的路线一定是最短的。

为什么“每次选最短”能带来质变?因为 BFS 保证每条边不会被无限次地增广。可以证明,一条边最多被增广 $O(V)$ 次,整体最多有 $O(VE)$ 轮增广;每一轮 BFS 要扫描所有边,所以总复杂度是:

Edmonds-Karp 是可靠的水管工:它不追求一次灌很多,而是每次都挑最近的一条水路,虽然慢一点,但绝不会失控。不过对大型管网来说,一次只走一条路还是太保守了,我们还需要能“批量送货”的算法。

O(VE2)\displaystyle O(VE^2)
Edmonds-Karp 用“每轮走最短路线”换来了确定性的多项式时间,不再看最大流的数值脸色。
  1. 增广轮数最短增广路策略下最多发生 $O(VE)$ 次增广。
    O(VE)\displaystyle O(VE)
  2. 总复杂度每轮 BFS 花费 $O(E)$,相乘得到 $O(VE^2)$。
    O(VE)×O(E)=O(VE2)\displaystyle O(VE) \times O(E) = O(VE^2)

Dinic 算法

更聪明的调度员想:与其一趟一趟送,不如先画一张“分层地图”。所谓分层,就是用 BFS 算出每个中转站离水库的最小跳数,把管网分成第 0 层、第 1 层、第 2 层……只允许水从第 $k$ 层流向第 $k+1$ 层,不允许在同一层或倒退。这张分层图让调度变得井井有条。

在分层图上,调度员用一次 DFS 同时沿着多条可行路线灌水,直到再也找不到从 $s$ 到 $t$ 的合法路径,这时的流量称为阻塞流。一个分层阶段结束后,再重新 BFS 分层,重复下去。此外还有“当前弧优化”:已经用完的边不再重复尝试,相当于水管工在清单上把放空的水管划掉。每一阶段复杂度为 $O(VE)$,最多有 $O(V)$ 个阶段,所以:

如果把 Edmonds-Karp 比作每次只送一车水,Dinic 就是按楼层图一次性规划出整条水路网络,让多个车队同时出发。它保留了 BFS 的稳定,又借助分层和批量增广跑得更快,是竞赛和工程中的默认选手。

O(V2E)\displaystyle O(V^2 E)
Dinic 的复杂度不依赖最大流数值,是理论优雅、实际也很快的主流算法。
  1. 每阶段效率一次分层图上找阻塞流,DFS 加当前弧优化后是 $O(VE)$。
    O(VE)\displaystyle O(VE)
  2. 阶段上界分层图的层数最多为 $V-1$,所以阶段不超过 $O(V)$。
    O(V)×O(VE)=O(V2E)\displaystyle O(V) \times O(VE) = O(V^2E)

最大流最小割定理

现在我们来到整座供水网络的“军事机密”。假设入侵者想切断一些管道,让水库的水一滴也到不了城市。这组被切断的管道称为一个割:管道集合切掉后,所有节点被分成两堆——源点那一侧记为 $S$,汇点那一侧记为 $T$,割容量就是从 $S$ 侧流向 $T$ 侧的所有管道容量之和。入侵者当然希望用最小的破坏成本达成目的,这就是最小割。

为什么最大流恰好等于最小割?先看一个直观事实:任何从 $s$ 到 $t$ 的水流,都必须跨过 $S$ 和 $T$ 之间的“边界线”。所以任意流的大小都不会超过任意割的容量,即:

更进一步,在最大流对应的残余网络中,一定不存在增广路径。令 $S$ 为源点在残余网络中能到达的所有节点,那么从 $S$ 到 $T$ 的正向边全部满流、反向边流量为零,于是这个割的容量恰好等于当前流的值,流值达到上界。因此:

这个定理像一则寓言:防守方辛苦设计的最大运力,恰好等于进攻方最省力的破坏点。理解它,不仅得到理论美感,也得到实用的计算工具——想求最小割,直接跑一遍最大流算法,再根据最后一次 BFS 的可达集把割边找出来即可。

∣f∣≤c(S,T)\displaystyle |f| \le c(S,T)
max⁡{∣f∣}=min⁡{c(S,T)}\displaystyle \max \{ |f| \} = \min \{ c(S,T) \}
最大流等于最小割,说的是“最大供水能力”和“最狠断水方案”在数值上完全相等。
  1. 上界关系水流必须经过割边,所以任意流不大于任意割容量。
    ∣f∣≤c(S,T)\displaystyle |f| \le c(S,T)
  2. 构造相等最大流的残余网络中取可达节点为 $S$,得到容量等于流值的割。
    ∣f∣=c(S,T)\displaystyle |f| = c(S,T)

经典应用

最大流与最小割的“攻守平衡”在 AI 和计算机科学中遍地开花。第一个经典场景是二分图匹配:左边是一组员工,右边是一组任务,每个人只能做自己擅长的任务,问最多能安排多少个任务。把每个员工、每个任务变成节点,源点连员工,任务连汇点,匹配关系连容量为 1 的边,最大流就是最大匹配数。

第二个场景是项目选择:公司有一些项目,每个项目有正收益或负成本,项目之间还有依赖关系——选了某个项目就不得不选另一个。这种“最大权闭合子图”问题可以构造一个源点连正收益项目、负成本项目连汇点的网络,让最小割给出最优取舍:

第三个场景是图像分割:把像素看作节点,相邻像素的相似程度看成边的容量,通过最小割把图像切成前景和背景两块。这正是将图像里的“像素点分类”问题统一成了“切断最不相似区域”的图割问题。从水管到像素,最大流最小割这把刀始终锋利,因为它把“调度”和“切割”都翻译成了同一个数学语言。

max⁡W∑v∈Ww(v)=∑w(v)>0w(v)−min⁡(S,T)c(S,T)\displaystyle \max_{W} \sum_{v \in W} w(v) = \sum_{w(v)>0} w(v) - \min_{(S,T)} c(S,T)
项目选择的最优解 = 所有正收益之和 - 最小割容量,本质是“放弃一部分,保住最大化”。
  1. 总正收益把所有正收益项目加起来,作为理论上限。
    ∑w(v)>0w(v)\displaystyle \sum_{w(v)>0} w(v)
  2. 最小割扣减最小割把必须放弃的边和副作用一并扣除。
    min⁡(S,T)c(S,T)\displaystyle \min_{(S,T)} c(S,T)

AI 应用场景

客服任务分配

阶段:任务调度与资源匹配

解决的问题:传统贪心匹配只能按顺序“先到先得”,遇到员工技能与任务多对多关系时容易错过全局最优。用网络流把员工、任务、可匹配关系建成容量为 1 的二分图,最大匹配数就是最多能派出的任务数,且同时能算出具体分配方案。

具体案例:某客服平台的技能标签包括“存款”“贷款”“信用卡”,不同客服掌握不同技能。将每个客服与任务建立可选边,源点汇点各接容量 1 的边,Dinic 跑一遍后得到最多同时处理的任务数,接起率提升约 12%。

广告项目组合优化

阶段:决策优化与预算分配

解决的问题:广告平台在预算有限时,要从众多候选投放方案中选出“收益最大且互斥约束满足”的组合。直接把问题建为最大权闭合子图,用最小割求解,比枚举组合数高效得多,也能解释每个项目为何被选中或放弃。

具体案例:某广告平台季度初有 30 个投放候选,彼此存在“必须同时上”的依赖关系。通过构造流量网络并求解最小割,算法在毫秒级给出最优组合,ROI 相比人工挑选提高 8%。

医学图像分割

阶段:图像后处理与能量最小化

解决的问题:像素级分类模型常常只关注局部,忽略相邻像素的相似性,导致分割边缘破碎。把像素间相似度建模为边的容量,用最小割做全局能量最小化,可以一次性得到平滑且全局一致的前景/背景分割。

具体案例:在 CT 影像肝脏分割任务中,先用深度学习生成像素概率图,再以概率构造网络流的边权,用最大流最小割做后处理,肝脏边界 Dice 系数从 0.91 提升到 0.94。

Python 演示

python63 行
from collections import deque class Dinic:    def __init__(self, n):        self.n = n        self.adj = [[] for _ in range(n)]     def add_edge(self, u, v, c):        # u 到 v 的正向边,容量为 c        self.adj[u].append([v, c, len(self.adj[v])])        # v 到 u 的反向边,容量为 0        self.adj[v].append([u, 0, len(self.adj[u]) - 1])     def bfs(self, s, t):        self.level = [-1] * self.n        q = deque([s])        self.level[s] = 0        while q:            u = q.popleft()            for e in self.adj[u]:                v, cap, rev = e                if cap > 0 and self.level[v] < 0:                    self.level[v] = self.level[u] + 1                    q.append(v)        return self.level[t] >= 0     def dfs(self, u, t, f):        if u == t:            return f        while self.it[u] < len(self.adj[u]):            e = self.adj[u][self.it[u]]            v, cap, rev = e            if cap > 0 and self.level[v] == self.level[u] + 1:                ret = self.dfs(v, t, min(f, cap))                if ret > 0:                    e[1] -= ret                    self.adj[v][rev][1] += ret                    return ret            self.it[u] += 1        return 0     def max_flow(self, s, t):        flow = 0        INF = 10 ** 9        while self.bfs(s, t):            self.it = [0] * self.n            while True:                f = self.dfs(s, t, INF)                if f == 0:                    break                flow += f        return flow # 与可视化动效一致的供水网络n = 4dinic = Dinic(n)dinic.add_edge(0, 1, 3)  # S -> Adinic.add_edge(0, 2, 2)  # S -> Bdinic.add_edge(1, 2, 1)  # A -> Bdinic.add_edge(1, 3, 2)  # A -> Tdinic.add_edge(2, 3, 2)  # B -> T print("max flow:", dinic.max_flow(0, 3))
目的:用 Dinic 算法求解一个小型供水网络的最大流,输出结果应等于 4,与可视化演示中的最大流一致。
  1. self.adj[u].append([v, c, len(self.adj[v])])正向边记录目标节点、容量和反向边索引。
  2. self.adj[v].append([u, 0, len(self.adj[u]) - 1])反向边初始容量为 0,专门用于“退流”。
  3. if cap > 0 and self.level[v] < 0:BFS 只走还有剩余容量的边,给节点分层。
  4. if cap > 0 and self.level[v] == self.level[u] + 1:DFS 严格沿层数加一的方向走,保证不绕圈。
  5. e[1] -= ret正向边消耗掉本次增广的流量。
  6. self.adj[v][rev][1] += ret反向边增加等量容量,允许之后后悔。
  7. while self.bfs(s, t):只要还能分层成功,就进入新的增广阶段。
  8. self.it = [0] * self.n每次阶段开始重置当前弧指针。

常见误区

误区残余网络里的反向边是多余的,增广路只要沿着原图方向找就行,走反向边不是绕远路了吗?

正解反向边是整个算法的“后悔药”。比如水先走了一条看起来不错的管道,后面发现占用了更优路线,反向边允许把水退回来重新安排。没有反向边,Ford-Fulkerson 很可能得到错误答案。反向边的容量等于已用流量,它让算法可以在残余网络上“撤销”之前的选择。

为何常见:初学者习惯把原图当作唯一现实,忘了残余网络是“可修改的流量空间”,而反向边正是修改的入口。

误区跑完最大流后,只要把所有已经满的边都割掉,就是最小割。

正解最小割要求割边后源点和汇点彻底不连通,而不是割掉所有满流边。正确的做法是:在最后一次 BFS 中,从源点沿仍有剩余容量的边能到达的节点构成 S 侧,其余为 T 侧,跨两侧的正向边才是最小割边。满流边只是割的必要条件之一,不是全部。

为何常见:最大流跑完后满流边很显眼,让人误以为“满的边就是瓶颈”,但割是集合概念,与源点可达性绑定。

误区只要不断找增广路,算法肯定会快速得到最大流,实际跑起来也不会太差。

正解Ford-Fulkerson 的复杂度是 $O(E|f^*|)$,如果最大流值很大,或者容量出现无理的实数,它可能增广极多次甚至不终止。它只是一个方法框架,必须搭配 BFS(Edmonds-Karp)或分层图(Dinic)才能保证多项式时间。

为何常见:因为小例子总是两三轮就收敛,让人误以为所有网络都这么温和。

误区Dinic 复杂度看着更优,所以任何图都用 Dinic,Edmonds-Karp 没有存在价值。

正解复杂度是渐进上界,实际表现还要看图的结构。在稀疏小图上,Edmonds-Karp 的常数小、实现简单,可能跑得更快;Dinic 需要维护分层和当前弧,代码更复杂。二者都是重要工具,应该根据网络规模选择。

为何常见:人们容易把“理论上界小”等同于“实际一定快”,忽略了常数和数据分布的影响。

面试题

Q1. 在一个流网络中,一条边的容量是 5,当前流量是 3,那么这条边的剩余容量是多少?

单选题 · easy

  • A.2← 正确答案
  • B.3
  • C.5
  • D.8

答案:A。剩余容量就是管径减去已用水量,即 5 - 3 = 2。就像一根能装 5 升的水管已经装了 3 升,还能再装 2 升。其他选项要么是当前流量,要么是总容量,要么是加和,都不符合“还能加多少水”的语义。

Q2. Ford-Fulkerson 方法的核心操作是什么?

单选题 · easy

  • A.拓扑排序
  • B.反复寻找增广路径← 正确答案
  • C.中序遍历
  • D.构建哈夫曼树

答案:B。Ford-Fulkerson 的思想是不断在残余网络中找从源点到汇点的增广路径,再沿路径推入瓶颈流量。拓扑排序是 DAG 的任务解法,中序遍历和哈夫曼树与网络流无关,所以选 B。

Q3. 在二分图匹配建模为最大流时,每条匹配边的容量通常设为多少?

单选题 · easy

  • A.0
  • B.1← 正确答案
  • C.源点容量
  • D.无穷大

答案:B。二分图匹配中一个员工最多匹配一个任务,所以从左到右的匹配边容量设为 1,保证流量只可能是 0 或 1。源点到左点、右点到汇点的边容量也为 1,最终最大流就是最大匹配数。

Q4. 最小割的准确定义是什么?

单选题 · mid

  • A.割掉一些边后源汇不连通,且这些边的容量总和最小← 正确答案
  • B.最小生成树中被切分的最小边
  • C.所有满流边构成的集合
  • D.源点到汇点的最短路径

答案:A。最小割是一个“割集”:把节点分成包含源点和不包含源点两侧,跨两侧的正向边容量之和最小。它要求“切断后不连通”,同时总容量最小。满流边集合不一定满足源汇不连通,最短路径更不是割。

Q5. 关于残余网络中的反向边,下列说法正确的是?

多选题 · mid

  • A.反向边允许算法回退之前的流量← 正确答案
  • B.反向边的容量等于对应正向边已使用的流量← 正确答案
  • C.反向边让算法可以修正之前的选择← 正确答案
  • D.反向边容量一定是负数

答案:ABC。反向边是“后悔药”:已用多少流量,反向边就能退回多少,所以容量等于正向边已使用流量;它让算法能回退旧选择,是正确性的关键。反向边容量是非负的,不可能为负,因此 D 错。

Q6. Edmonds-Karp 相比原始 Ford-Fulkerson 的主要改进是什么?

单选题 · mid

  • A.用 DFS 随机找增广路
  • B.用 BFS 每次找最短增广路← 正确答案
  • C.用堆优化最短路径
  • D.直接计算最小割

答案:B。Edmonds-Karp 用 BFS 在残余网络中找边数最少的增广路,从而保证增广次数有界,复杂度为 $O(VE^2)$。DFS 随机找路不能保证多项式时间;堆优化通常用于最短路而不是 BFS。

Q7. 关于最大流最小割定理,下列说法正确的是?

多选题 · mid

  • A.最大流的值等于最小割的容量← 正确答案
  • B.最小割等于最小生成树的割
  • C.任意可行流的值一定小于任意割的容量
  • D.存在一个最大流,它对应当前残余网络中的某个最小割← 正确答案

答案:AD。定理说最大流 = 最小割,且任意可行流都不超过任意割容量,所以 C 的“小于”应改为“小于等于”。B 混淆了图论概念。D 正确:跑完最大流后,从源点在残余网络中可达的点集可构造出对应的最小割。

Q8. Dinic 算法中“阻塞流”指的是什么?

单选题 · hard

  • A.当前分层图上无法继续增广的流← 正确答案
  • B.只包含一条边的流
  • C.汇点无法接收的流量
  • D.所有满流边组成的最小割

答案:A。Dinic 在每一阶段构造分层图,并在分层图上用 DFS 多路增广,直到不存在从源到汇的合法路径,此时达到阻塞流。它不是单条边或单个割,而是“本层结构下的最大可推水量”。

Q9. 一个有 $n$ 个节点、$m$ 条边的流网络,Dinic 算法最坏时间复杂度是多少?

单选题 · hard

  • A.$O(mn)$
  • B.$O(n^2 m)$← 正确答案
  • C.$O(m^2 n)$
  • D.$O(nm^2)$

答案:B。Dinic 每个分层阶段用一次 BFS 加一次 DFS 求阻塞流,消耗 $O(nm)$;最多有 $O(n)$ 个阶段,相乘得到 $O(n^2m)$。它不依赖最大流的数值大小,这是它优于纯增广路算法的重要原因。

Q10. 图像分割中,图割的边的容量通常用来刻画什么?

单选题 · mid

  • A.每个像素的颜色值
  • B.相邻像素的相似度或分割惩罚← 正确答案
  • C.整张图像的压缩率
  • D.边缘检测算子的数量

答案:B。图割把像素作为节点,相邻像素之间边的容量越小,说明它们越不相似,越容易被分割开;边的容量就是“分开它们的代价”。颜色值只是节点属性,压缩率和边缘算子数量都与能量函数无关。