先把结论放在前面:Dinic算法是目前最大流问题里综合表现最稳的一类算法,复杂度分析给出的上界是 O(V^2E),其中 V 是顶点数,E 是边数。很多初学者看到这个上界会被吓到,觉得不如某些看似更优的算法,但实际跑起来它往往比理论界快得多。这篇文章我不打算只堆结论,而是把 Dinic 算法的每一步机制、复杂度上界是怎么推导出来的、证明过程中的关键引理,以及我在实际做题和工程实现里踩过的坑,全部拆开讲清楚。适合正在学网络流、准备竞赛、或者需要在生产环境里处理最大流/最小割问题的朋友。
1. 从一个网络流问题说起:Dinic算法到底解决什么
1.1 最大流问题的核心矛盾
最大流问题描述起来很简单:给定一个有向图,每条边有容量上限,给定源点 s 和汇点 t,求从 s 到 t 能运送的最大流量。但“简单描述”背后藏着两个麻烦:一是图规模可能很大,二是增广路径的选择顺序会严重影响效率。
最朴素的 Ford-Fulkerson 方法每次找一条 s 到 t 的路径,然后沿路径推送流量,直到不存在这样的路径。问题在于,如果每次选的路径很差,可能需要增广非常多次。举个极端例子,一条容量很大的边反复被“错误路径”占用,又要靠反向边退回来,来回折腾,复杂度可能和容量值成正比。遇到容量是 10^9 级别的数据,这种算法直接就废了。
Edmonds-Karp 算法用 BFS 找最短增广路,把增广次数限制到了 O(VE),这是第一次从“看脸”变成“有理论保证”。但它每次 BFS 只找一条增广路,找完一条就要重新跑一遍全图 BFS,边的扫描次数仍然很高。
Dinic 算法的核心突破在于:一次 BFS 建好分层图,然后在分层图上用 DFS 多路增广,尽可能把当前分层图里的“阻塞流”一次推完,再重新 BFS。这个设计同时解决了“增广次数太多”和“重复扫描太多”两个痛点。
1.2 Dinic算法相比EK算法的关键改进
EK 算法本质上是一趟 BFS 配一次增广,Dinic 是一趟 BFS 配一次“阻塞流推送”。什么叫阻塞流?就是在当前分层图中,已经不存在从 s 到 t 的、沿着层次递增方向走到底的增广路了。注意这里说的是“分层图中不存在”,不代表原残量网络中不存在,只是说下一轮需要重新分层。
这个改进带来的直接效果是:每一轮 BFS 能榨干当前分层图的全部价值,而不是只取一条路。用生活化的例子理解,EK 像是你每次只背一桶水,来回跑;Dinic 则是先修好一条有层次的“水管网络”,然后尽量让所有管道同时流水,流不动了再重新规划。
从复杂度分析角度,EK 的增广次数是 O(VE),每次增广要 O(E) 的 BFS 开销,总复杂度 O(VE^2)。Dinic 的阶段数被证明是 O(V),每个阶段做阻塞流需要 O(VE),总复杂度 O(V^2E)。虽然从形式上看 V^2E 和 VE^2 在不同图下各有优劣,但实际表现中 Dinic 的常数小、剪枝能力强,绝大多数场景下都更实用。
1.3 复杂度结论先摆出来
先把几个关键结论列出来,后面逐一证明:
- 普通容量网络下,Dinic 算法时间复杂度为 O(V^2E)。
- 单位容量网络(每条边容量为 1)下,复杂度可以收紧到 O(E sqrt V)。
- 二分图最大匹配场景下,等价于单位容量网络上的 Dinic,因此也是 O(E sqrt V),这正是 Hopcroft-Karp 算法的复杂度来源。
- 实际工程中,Dinic 的当前弧优化、多路增广、gap 优化等技巧,能让它在绝大多数随机图上远低于理论上界运行。
这些结论不是背下来的,而是要从阻塞流、分层图、层次距离单调性这几个概念一步步推出来。
2. 算法机制拆解:BFS分层与DFS增广
2.1 分层图:把残量网络变成有向无环层状结构
Dinic 的每一轮从 BFS 开始。BFS 从源点 s 出发,在残量网络上计算每个顶点到 s 的最短距离,记作 level[v]。只保留满足 level[v] = level[u] + 1 的边,就得到一张分层图,也叫层次图。
为什么只保留“跨一层”的边?因为这样能保证在分层图里走的任何一条路,都是从 s 到 t 的最短路。更重要的是,分层图天然是有向无环的,所有边都从低层指向高层,不会出现环。DFS 在 DAG 上搜索,天然不需要担心死循环。
这里有一个容易忽略的细节:分层图里的边,容量仍然是残量容量,而不是原始容量。也就是说,已经满流的边不会出现,反向边如果还有容量,也可能出现在分层图里,但反向边会使 level 下降,所以在严格按 level+1 前进的规则下,反向边一般不会被使用。这其实是 Dinic 正确性的重要基石:通过反向边“退流”的能力,在后续轮次的分层中依然存在。
2.2 DFS多路增广的实现思路
BFS 分完层之后,从 s 开始做 DFS,只沿 level[v] = level[u] + 1 的边前进,目标是到达 t。一次 DFS 成功到达 t 后,并不立即停止,而是继续尝试从当前节点推送更多流量,只有当某个节点再也推不出流量时,才回溯。
这就是“多路增广”的含义:每个节点累加所有下游子路径返回的流量,如果能推送的总流量大于 0,就返回给上游;否则返回 0。这样一次 DFS 调用就可能完成多条路径的增广。
我见过不少初学者把 Dinic 的 DFS 和普通 DFS 搞混,以为每次只找一条路。实际上,Dinic 的多路增广是它比 EK 快很多的核心原因之一。用伪代码表示大概是:
function dfs(u, pushed): if u == t: return pushed for each edge e from u while pushed > 0: if level[e.to] == level[u] + 1 and e.cap > 0: tr = dfs(e.to, min(pushed, e.cap)) if tr > 0: e.cap -= tr e.reverse.cap += tr return tr return 0注意这里为了展示核心逻辑,我简化成了“每找到一个可行子路径就返回”,实际优化版本会在一个节点内循环累加所有子路径流量,把所有能推送的流量都推完再返回。这个区别在后文的复杂度分析中很关键。
2.3 当前弧优化到底优化了什么
当前弧优化可能是 Dinic 里最出名的优化手段。它的核心思想是:在一个阶段内,如果某个节点的某条出边已经被 DFS 尝试过,并且没能推送流量,那么在本次分层图中,这条边以后也不会再成功,所以可以直接跳过。
为什么“以后也不会成功”?因为 DFS 失败意味着这条边的下游方向已经无法到达 t,或者边的容量已经被榨干。在当前分层图结构不变的前提下,继续尝试同样的边只会重复失败。因此每个节点维护一个 cur[u] 指针,指向下一条还没被放弃的出边,每次只从 cur[u] 开始尝试。
这个优化看起来只是“跳过失败边”,但它把单阶段内每个节点的出边扫描次数从 O(E) 级别降到了 O(度) 级别,对复杂度常数影响非常大。没有当前弧优化的 Dinic,在一张稠密图里可能退化得很难看;有了它,才能稳定达到 O(VE) 的单阶段上界。
3. Dinic算法复杂度分析完整推导
3.1 单阶段内的时间上界
先把每个阶段的代价算清楚。一个阶段包括一次 BFS 和一次阻塞流推送。BFS 需要扫描残量网络中的每条边,代价 O(E)。
阻塞流部分用 DFS 完成。关键问题是:这个 DFS 过程一共会调用多少次“有效的深度搜索”?我采用一种便于理解的计数方式:每次从某个节点出发的 DFS 返回时,只有两种情况。
第一种情况,DFS 成功推送了流量。那么在这条成功路径上,至少有一条边会在本次推送后容量变为 0。也就是说,一次成功推送至少会“消灭”一条边。
第二种情况,DFS 返回 0,即该节点在分层图中已经无法到达 t。这时当前节点正在尝试的出边会被当前弧指针跳过,相当于“消灭”了一条候选边。
综合来看,不管是成功还是失败,每次 DFS 调用都会导致至少一条边被标记为“无用”。一张分层图里最多有 E 条边,所以单阶段内 DFS 调用的次数上界是 O(E)。每次 DFS 递归深入,路径长度受层次限制,最多 O(V) 层,所以单阶段阻塞流部分复杂度为 O(VE)。
还有一个细节要补充:如果某个节点成功推送后又继续尝试其他出边,那么它可能在同一层调用中多次进入 DFS。但每次进入要么饱和一条边,要么前移一次当前弧指针,计数规则不变,整体上界依然是 O(E) 次调用。当前弧优化在这里不是可有可无的,它正是保证“每条边最多被跳过一次”的关键。
加上 BFS 的 O(E),单阶段总复杂度就是 O(VE)。
3.2 阶段数量为什么是 O(V)
接下来要证明阶段数不会太多。每一轮 BFS 会得到一个新的 level[t],也就是 s 到 t 在当前残量网络中的最短距离。我要证明一个核心结论:每执行完一个阶段,level[t] 严格变大。
先回顾分层图的性质。分层图由满足 level[v] = level[u] + 1 的边构成,阻塞流推送完成后,分层图中不存在从 s 到 t 的路径。
现在考虑下一轮的 BFS 距离,记为 level'[t]。假设 level'[t] 没有变大,也就是 level'[t] = level[t]。那么在新的 BFS 距离下,任意一条从 s 到 t 的最短路径,其长度等于 level[t]。同时,对于残量网络中的任意一条边 (u, v),都有 level'[v] 不超过 level'[u] + 1。沿着一条最短路径逐步展开,可以推出这条路径上的每一步实际上都满足 level'[v] = level'[u] + 1,否则路径长度不可能恰好是 level[t]。
但这里要小心:上一轮的 level 和这一轮的 level' 是不同的。为了更严谨,通常的做法是利用“层次函数单调不减”这个性质。可以证明,在阻塞流推送之后,每个顶点的 level 在下一轮 BFS 中只可能增大或保持不变,不可能减小。原因是所有使 level 下降的反向边,只能在那些“上一轮中 level 较小的顶点”之后被创建,这个性质保证了单调性。
在单调性的基础上,假设 level'[t] = level[t]。那么新的最短路径上的每一条边都必须满足 level[v] = level[u] + 1,也就是这条路径在旧分层图中也存在。但这与“旧分层图中已经没有 s 到 t 的路径”矛盾。因此 level'[t] 不可能等于 level[t],只能是严格大于。
level[t] 至少从 1 开始,最大不会超过 V-1(一条简单路径最多经过 V 个顶点),所以阶段数最多是 V-1,也就是 O(V)。
3.3 总复杂度 O(V^2E) 的合成
现在把两个结论合起来。每个阶段耗时 O(VE),阶段数 O(V),所以总复杂度:
O(V) × O(VE) = O(V^2E)
这就是 Dinic 算法最经典的复杂度上界。需要强调,这个上界是“最坏情况”的理论上界。它并不代表算法每次运行都会这么慢,只保证不会比这个量级更差。
很多教材和文章直接给出 O(V^2E) 却不解释,导致读者把 Dinic 想得很慢。实际上,对于稀疏图,V^2E 的量级并不夸张;对于稠密图,结构上的限制往往让层次数远小于 V。理论界和实际表现的差距,我会在第 5 节专门说明。
3.4 二分图匹配特例 O(E sqrt V) 的补充说明
如果所有边容量都是 1,也就是单位容量网络,Dinic 的复杂度可以进一步分析。在这一类图中,每次增广至少让一条边饱和,但更重要的是,层次距离 level[t] 的增长速度有更强的保证。
可以证明,在单位容量网络上,当阶段数达到 O(sqrt V) 之后,剩余阶段中每阶段能推送的流量至少和当前最短路径长度有关,最终总阶段数被 O(sqrt V) 控制。具体推导涉及“短增广路”和“长增广路”的分界:较短路径阶段数量是 O(sqrt V),较长路径阶段数量也是 O(sqrt V),加起来还是 O(sqrt V)。
每个阶段依然耗费 O(E),于是单位容量图上 Dinic 的复杂度是 O(E sqrt V)。二分图最大匹配通过源点连左部、右部连汇点、中间边容量全为 1 的方式建模,正好属于单位容量网络,因此复杂度同样是 O(E sqrt V)。这就是 Hopcroft-Karp 算法为什么能达到 O(E sqrt V) 的本质原因——它本质上就是单位容量图上的 Dinic。
4. 复杂度证明中的几个关键引理
4.1 阻塞流的定义与存在性
前面反复提到“阻塞流”,这里给一个更严格的定义:在分层图 G_L 中,一个流称为阻塞流,如果它推送完成后,G_L 中不存在从 s 到 t 的路径。注意,阻塞流不一定需要让 s 到不了任何点,只需要 s 到不了 t 即可。
存在性不用怀疑,因为算法本身构造了一个:不断在 G_L 中找 s 到 t 的路径并推送,直到找不出路径,得到的一定是阻塞流。问题在于效率,如果每次只找一条路径,那复杂度就不好控制。Dinic 的多路增广本质上是在高效地构造这个阻塞流。
理解阻塞流的另一个关键是:它和“最大流”不是一回事。阻塞流只是在当前分层图中的局部最优,全局最大流可能要等后续重新分层后,通过反向边调整才能达到。这也是为什么 Dinic 需要多轮 BFS-DFS 循环,而不能一轮搞定。
4.2 层次距离严格递增的证明思路
外层循环次数依赖的核心引理是“level[t] 每轮严格递增”。这个引理的完整证明需要两个步骤:
第一步,证明每个顶点的层次距离在下一轮 BFS 中不会变小。这一步通常用反证法:如果某个顶点 v 的 level'[v] < level[v],那么在新的 BFS 最短路中,v 的前驱 u 满足 level'[u] = level'[v] - 1。考虑这条路径的上一条边,结合残量网络边的性质,可以推出矛盾。这一步比较绕,但核心直觉是:残量网络中新出现的反向边只会从“较深”的顶点指回“较浅”的顶点,不会制造出更短的 s-v 路径。
第二步,假设 level'[t] = level[t],证明这会导致旧的 s-t 路径存在。这个我在 3.2 节已经给出直观版本。两条合起来,level'[t] 只能严格大于 level[t]。
这一步是整个复杂度分析中最容易出错的地方。我见过不少证明直接忽略“层次距离单调不减”这一环,默认新最短路径一定在旧分层图中,这在严格性上是有漏洞的。
4.3 证明过程中最容易忽略的细节
细节一:BFS 分层时必须在残量网络上进行。很多初学者在实现时忘了检查容量是否大于 0,导致分层图中出现满流边,DFS 时反复尝试失败,复杂度分析就全毁了。
细节二:反向边的 level 问题。阻塞流推送后,正向边容量减少,反向边容量增加。反向边自然会让 level 下降,所以在当前分层图中不会被访问,但会影响后续 BFS 的分层结果。这一点在证明层次距离单调性时尤其重要。
细节三:复杂度证明中的“每次 DFS 至少消灭一条边”,必须依托当前弧优化。如果没有当前弧优化,同一个节点可能反复用一条无法到达 t 的边做无用递归,调用次数就无法用 O(E) 约束。所以严格来说,教科书上的 O(V^2E) 上界是“带当前弧优化版 Dinic”的上界。
5. 实战中的复杂度表现与优化策略
5.1 实际运行远优于理论界的原因
理论上的 O(V^2E) 是一个非常保守的上界。实际运行中,层次距离往往增长得比理论证明还快,阶段数通常远小于 V。对于随机生成的图,绝大多数情况下阶段数是个位数到十几个,而不是 V 的数量级。
另外,DFS 在分层图中沿最短路径前进,路径长度本身有限。多路增广一次推送往往能同时饱和多条边,大大减少了 DFS 调用次数。当前弧优化又把大量“无效边”的扫描直接跳过。三重因素叠加,让 Dinic 的实际速度非常可观。
我用一个经验值来说明:在常见的数据规模下,比如 V 在 10^4 到 10^5、E 在 10^5 到 10^6 的网络流建模题里,Dinic 通常能在几十到几百毫秒内跑完。相比之下,EK 在相同规模下基本不可用。这就是为什么 Dinic 是竞赛和工程里的默认选择之一。
5.2 常见优化手法的复杂度影响
除了当前弧优化,还有几个常用优化:
多路增广:在同一个节点内累加所有子路径的流量,而不是找到一个就返回。这个优化让一次 DFS 调用能推送多条路径的流量,减少函数调用开销。
gap 优化:统计每一层的顶点数量,如果某一层没有顶点,说明 s 和 t 已经断层,直接终止算法。这个优化不改变复杂度上界,但能提前退出。
容量为 0 的边直接跳过:看似微不足道,但在稠密图上能减少大量判断。
使用邻接表存边并且成对存储反向边:这让反向边更新更方便,也不影响复杂度,但实现细节会决定常数大小。
这些优化的共性是不改变算法最坏复杂度,但大幅改善平均表现。尤其当前弧优化和多路增广,几乎是 Dinic 的标配,建议直接写进模板里。
5.3 什么时候该换算法
Dinic 不是万能的。虽然理论界 O(V^2E) 在大多数情况下表现优秀,但遇到特殊构造的图,确实可能逼近最坏情况。比如一些专门卡 Dinic 的图,层次数会变得非常多,或者每一层可推送的流量非常少,导致阶段数接近 V。
遇到这类情况,可以考虑的替代方案包括:
- 容量很小的图或者需要多次查询的图,可以考虑预流推进(Push-Relabel),它的渐进复杂度在某些图上更强。
- 平面图上的最大流,有基于对偶图的更优算法。
- 二分图匹配如果规模极大,可以直接用 Hopcroft-Karp,它本质上是单位容量流,常数更小。
- 如果边容量是浮点数,Dinic 依然可用,但要注意浮点数比较的精度问题,通常设置 eps。
从工程角度,我建议把 Dinic 作为默认武器,遇到卡时间的图再针对性地换算法。
6. 常见问题与踩坑记录
6.1 当前弧数组忘记重置
这是实现 Dinic 时最高频的 bug。当前弧优化只在同一个 BFS 分层阶段内有效。每次 BFS 重新分层后,所有节点的 cur 指针都要重置为 0。
我见过不少同学理解了这个优化之后,把 cur 数组在 while 循环外只初始化一次,结果第二次 BFS 后 DFS 直接跳过大量边,答案错得离谱。排查方法很简单:在每次 BFS 后,用 memset 或循环把 cur[u] 重置为 head[u]。
6.2 BFS在残量网络中误判不可达
另一个常见坑是 BFS 只遍历正向边,忽略了反向边。在残量网络中,反向边也是真实存在的边,容量代表可以退回的流量。如果不考虑反向边,BFS 可能过早判断 s 和 t 不连通,导致算法提前终止,输出错误的最大流。
正确做法是 BFS 时遍历所有邻接边,判断条件只有一个:容量大于 0。不要区分正向边和反向边,它们都是残量网络的一部分。
6.3 递归深度爆栈
Dinic 的 DFS 递归深度等于当前分层图中 s 到当前节点的距离,最坏情况下可以达到 V 的量级。当 V 很大,比如 10^5 到 10^6 时,系统默认的递归栈可能不够用,程序会直接崩溃。
解决方案有三种:一是手动把 DFS 改成非递归形式,但实现复杂度较高;二是在支持大栈的平台上增大栈空间;三是在设计算法时尽量避免极端深度的图。如果只是做竞赛题,很多评测环境栈较大,但生产环境不可控,建议提前评估。
6.4 复杂度分析的常见误解
有一个流传比较广的误解是“Dinic 的最坏复杂度是 O(V^2E),所以它比 EK 更慢”。这个推断是错的,因为两者的上界形式不同,适用图也不同。在稠密图中 E 接近 V^2,EK 是 O(V^4) 级别的上界,而 Dinic 是 O(V^4) 也是? 这里插一句:当 E=O(V^2) 时,EK 上界 O(VE^2)=O(V^5),Dinic O(V^2E)=O(V^4)。Dinic 无论如何都不比 EK 差一个量级。
另一个误解是“加了优化之后复杂度上界会变好”。实际上当前弧优化、多路增广主要改善常数和实际表现,不会改变 O(V^2E) 这个最坏上界。唯一从根本上改进上界的,是单位容量网络这类特殊场景下的分析。
还有一点容易被忽略:复杂度分析里的 E 指的是残量网络中的边数。如果实现时把每条无向边拆成两条有向边,又额外加了反向边,那么 E 会翻倍甚至更多。这意味着在无向图上,实际复杂度的常数也要相应放大。
根据我自己的经验,写 Dinic 时最好的习惯是:先把朴素版跑通,再依次加上当前弧和多路增广,每加一个优化都用小规模数据对拍确认结果不变。这样既能在出错时快速定位,也能对每个优化的作用有直观感受。复杂度分析给的是安全边界,真正的速度感受,还是要靠大量实测。