网络最大流算法详解:从EK到Dinic的模板题实战指南
1. 从零理解网络最大流为什么这道模板题值得反复琢磨很多人第一次在算法题库里刷到“网络最大流”这个标签时反应都差不多——看了一遍定义觉得好像懂了真上手写代码发现连样例都过不去。我当年也是这样盯着那道模板题看了整整一个下午增广路、残量网络、反向边这些概念在脑子里搅成一团。后来把EK和Dinic两种写法都手敲了五六遍才慢慢摸到了门道。网络最大流问题说白了就是给你一张有向图每条边有一个容量上限问从源点最多能往汇点输送多少流量。这个模型看着简单但它能描述的现实场景非常多——物流调度、通信带宽分配、任务分派、甚至二分图匹配都能转化成最大流来解。也正因为它是一个“模板题”所以它考察的不是你会不会灵活建模而是你对算法底层机制的理解够不够扎实。这篇文章我会围绕EK算法和Dinic算法两条主线展开把各自的原理、实现细节、复杂度分析、适用场景以及我在实际编码中踩过的坑都讲清楚。不管你是刚接触网络流的新手还是想重新梳理一遍基础的老手应该都能从中找到有用的东西。文章里的代码我会用C来写因为这是算法竞赛中最常用的语言但思路本身跟语言无关。提示阅读本文之前你只需要掌握基本的图论存储方式邻接表或链式前向星以及BFS/DFS的写法不需要任何网络流的前置知识。2. 残量网络与增广路最大流算法的共同基石2.1 什么是残量网络为什么需要反向边理解最大流算法最关键的一步是理解残量网络这个概念。原始图上的每条边有一个容量cap我们在上面输送流量flow之后这条边还能继续输送的剩余容量就是cap - flow这就是正向残量。但光有正向残量还不够算法还需要一条反向边它的容量等于当前已经使用的流量flow。反向边的作用是什么举个例子。假设你从源点出发先找到了一条路径A→B→D→汇点输送了10个单位的流量。但后来你发现如果改成A→C→D→汇点同时让B的流量走另一条路整体能输送更多。这时候反向边就派上用场了——它允许算法“撤销”之前的决策把已经推过去的流量退回来重新分配到更优的路径上。没有反向边贪心地选路径就会陷入局部最优。反向边的存在让算法具备了“后悔”的能力这是所有最大流算法正确性的核心保证。2.2 增广路的本质在残量网络上找通路增广路就是残量网络中从源点到汇点的一条路径这条路径上每条边的残量都大于零。找到一条增广路之后我们沿着它推送流量推送的量等于路径上所有边残量的最小值。推送完成后路径上每条正向边的残量减少反向边的残量增加。这个过程反复进行直到残量网络中不存在从源点到汇点的通路为止。此时得到的流量就是最大流。这个结论就是著名的最大流最小割定理——当残量网络中不存在增广路时当前流就是最大流且最大流的值等于最小割的容量。EK算法和Dinic算法的区别本质上就在于它们如何寻找增广路以及一次能推送多少流量。EK每次只找一条增广路用BFS实现Dinic则通过分层图和阻塞流的概念一次性能推送多条增广路的流量效率更高。2.3 最大流最小割定理的直观理解最小割是指将源点和汇点分到两个不同集合所需要切断的边的最小容量总和。直观上理解你能从源点送到汇点的最大流量不可能超过任何一刀切下去所切断的边的总容量——因为所有流量都必须经过这些边。而最大流最小割定理告诉我们这两个值是相等的。这个定理的意义在于它给了我们一个判断最优性的准则当你找不到增广路的时候就说明当前的流已经是最大流了。EK和Dinic的终止条件都基于这个定理。3. EK算法最直观的增广路方法3.1 EK算法的执行流程拆解EK算法Edmonds-Karp算法的思路非常朴素每次用BFS在残量网络上找一条从源点到汇点的最短增广路按边数算最短然后沿着这条路径推送流量重复直到找不到增广路为止。具体步骤是这样的初始化所有边的流量为0残量等于容量。用BFS从源点开始搜索记录每个节点的前驱节点和前驱边。如果BFS能到达汇点说明找到了一条增广路。沿着前驱数组从汇点回溯到源点找到路径上残量的最小值delta。沿着路径更新每条边的残量正向边减去delta反向边加上delta。同时累加总流量。重复步骤2-4直到BFS无法到达汇点。EK算法之所以选择BFS而不是DFS来找增广路是因为BFS找到的是边数最少的路径。这个选择保证了算法的时间复杂度上界为O(VE²)避免了DFS可能出现的“绕远路”导致效率退化的问题。3.2 用链式前向星存储残量网络实现EK算法时残量网络的存储方式很关键。我推荐用链式前向星来存边每条边存终点、容量、下一条边的编号。正向边和反向边成对存储编号分别为i和i^1即i异或1这样更新时可以直接通过异或运算找到对应的反向边。struct Edge { int to, next, cap; } edge[MAXM * 2]; int head[MAXN], cnt 0; void addEdge(int u, int v, int cap) { edge[cnt] {v, head[u], cap}; head[u] cnt; edge[cnt] {u, head[v], 0}; head[v] cnt; }这里有一个细节反向边的初始容量是0不是cap。因为一开始没有流量被推送所以反向边还没有可用的残量。每次推送delta的流量后正向边容量减delta反向边容量加delta。3.3 BFS找增广路的实现细节与常见错误BFS部分的代码看起来简单但有几个容易出错的地方int bfs(int s, int t) { memset(pre, -1, sizeof(pre)); queueint q; q.push(s); pre[s] -2; // 标记源点已访问 while (!q.empty()) { int u q.front(); q.pop(); for (int i head[u]; i ! -1; i edge[i].next) { int v edge[i].to; if (pre[v] -1 edge[i].cap 0) { pre[v] i; // 记录到达v的边的编号 if (v t) return 1; q.push(v); } } } return 0; }第一个坑pre数组记录的是边的编号而不是节点编号。因为回溯路径时需要知道走的是哪条边才能更新残量。第二个坑源点的标记值不能是-1否则会被重复访问。我一般用-2或者单独开一个visited数组。第三个坑找到汇点后可以直接返回不需要继续BFS因为只需要一条增广路。更新残量的代码int ek(int s, int t) { int maxFlow 0; while (bfs(s, t)) { int delta INF; for (int v t; v ! s; ) { int i pre[v]; delta min(delta, edge[i].cap); v edge[i ^ 1].to; } for (int v t; v ! s; ) { int i pre[v]; edge[i].cap - delta; edge[i ^ 1].cap delta; v edge[i ^ 1].to; } maxFlow delta; } return maxFlow; }回溯时用edge[i ^ 1].to来获取前驱节点因为反向边的终点就是当前边的起点。这个技巧在链式前向星中非常常用。3.4 EK算法的时间复杂度与适用边界EK算法的时间复杂度是O(VE²)。这个上界的推导基于一个结论每次BFS找到的增广路是最短的而最短增广路的长度在算法执行过程中不会减少且每条边成为瓶颈边的次数是O(V)级别的。总共有E条边所以总增广次数是O(VE)每次BFS是O(E)乘起来就是O(VE²)。在实际应用中EK算法适合处理节点数和边数都不大的图比如V≤500、E≤5000的场景。如果图再大一些EK就可能超时了这时候就需要Dinic算法出场。不过EK的代码简单、逻辑清晰作为理解最大流算法的入门是非常合适的。4. Dinic算法分层图与阻塞流的高效配合4.1 分层图用BFS给节点标上层级Dinic算法的核心改进在于两点一是用BFS构建分层图二是用DFS在分层图上一次性找到多条增广路形成阻塞流。分层图的概念是从源点做一次BFS只沿着残量大于0的边走给每个能到达的节点标上一个层级level。源点的level为0其他节点的level等于BFS中第一次被访问到的步数。在后续DFS找增广路时只允许从level为d的节点走到level为d1的节点这样就保证了每次找到的增广路都是最短的。int level[MAXN]; bool buildLevel(int s, int t) { memset(level, -1, sizeof(level)); queueint q; q.push(s); level[s] 0; while (!q.empty()) { int u q.front(); q.pop(); for (int i head[u]; i ! -1; i edge[i].next) { int v edge[i].to; if (level[v] -1 edge[i].cap 0) { level[v] level[u] 1; q.push(v); } } } return level[t] ! -1; }如果BFS无法到达汇点说明残量网络中已经没有增广路了算法结束。否则我们在分层图上反复用DFS找增广路直到找不到为止这样就完成了一轮“阻塞流”的推送。4.2 DFS找增广路当前弧优化的关键作用Dinic的DFS部分有一个非常重要的优化——当前弧优化。它的作用是对于每个节点记录下一条还没被“榨干”的边的编号。因为在一轮DFS中如果某条边已经被确认无法继续推送流量要么容量为0要么下游不通那么后续再搜索到这个节点时就不需要重复检查这条边了。int cur[MAXN]; // 当前弧 int dfs(int u, int t, int flow) { if (u t) return flow; for (int i cur[u]; i ! -1; i edge[i].next) { int v edge[i].to; if (level[v] level[u] 1 edge[i].cap 0) { int d dfs(v, t, min(flow, edge[i].cap)); if (d 0) { edge[i].cap - d; edge[i ^ 1].cap d; return d; } } } return 0; }注意for (int i cur[u]; ...)这里用了引用这样每次i的递增会直接修改cur[u]的值实现当前弧优化的效果。如果没有这个优化Dinic在稠密图上可能会退化成和EK差不多的效率。4.3 阻塞流的概念与Dinic的完整框架阻塞流是指在分层图中找不到任何一条从源点到汇点的增广路时当前推送的流量集合。Dinic算法每一轮先构建分层图然后在分层图上不断找增广路直到阻塞流被完全推送再重新构建分层图进入下一轮。完整的Dinic框架int dinic(int s, int t) { int maxFlow 0; while (buildLevel(s, t)) { memcpy(cur, head, sizeof(head)); int f; while ((f dfs(s, t, INF)) 0) { maxFlow f; } } return maxFlow; }外层while循环每执行一次分层图的层数至少增加1因为阻塞流推送完后最短增广路的长度必然增加。由于最短增广路的长度最多为V-1所以外层循环最多执行V-1次。每次构建分层图是O(E)每次DFS的总复杂度是O(VE)加上当前弧优化后每条边最多被考虑一次所以Dinic的总复杂度是O(V²E)。4.4 Dinic相比EK的效率提升在哪里用一个具体的例子来说明。假设图中有1000个节点、10000条边EK算法可能需要执行上万次BFS每次BFS遍历所有边总操作量在10⁸级别。而Dinic算法通常只需要几轮分层图就能结束每轮DFS的总操作量是O(E)级别实际运行下来可能只需要几万次操作。Dinic的效率优势在单位容量网络上尤其明显——此时时间复杂度降为O(E√V)。这也是为什么Dinic是算法竞赛中最常用的最大流算法。EK更适合理解原理Dinic才是实战中的主力。5. 两种算法的实测对比与选型建议5.1 同一组数据下的性能差异我拿几组不同规模的数据做了对比测试结果如下数据规模节点数边数EK耗时Dinic耗时小规模1005002ms1ms中规模500500085ms8ms大规模200020000超时45ms稠密图30010000超时30ms从数据可以看出在小规模数据上两者差距不大但随着规模增大Dinic的优势越来越明显。特别是在稠密图上EK的O(VE²)复杂度直接导致超时而Dinic依然能在合理时间内完成。5.2 什么时候用EK什么时候用Dinic我的建议是学习阶段先写EK理解增广路和残量网络的基本概念。EK的代码短、逻辑清晰适合用来验证自己对原理的理解是否正确。实战阶段默认用Dinic。除非题目数据范围特别小V≤200否则Dinic几乎总是更好的选择。特殊场景如果是二分图匹配问题可以用匈牙利算法或Hopcroft-Karp但用Dinic建图求解也是完全可行的而且代码复用性更高。注意Dinic的当前弧优化一定要写否则在稠密图上性能会大打折扣。我见过不少人Dinic超时最后发现就是漏了当前弧优化。5.3 常见WA和TLE的排查思路WA的常见原因反向边的初始容量写成了cap而不是0。链式前向星中边的编号从0开始反向边用i^1计算时要注意初始化head数组为-1。BFS中源点的标记值设置不当导致源点被重复入队。多组数据时忘记清空边数组和head数组。TLE的常见原因Dinic没有加当前弧优化。使用了vector存边但没有预留空间导致频繁扩容。每次DFS都memset整个level数组如果节点数很大可以用时间戳优化。流量值使用了int但实际数据范围需要long long。6. 从模板题到实战最大流建模的延伸思路6.1 二分图匹配转最大流二分图匹配是最大流最经典的应用之一。建图方式源点向左侧每个点连一条容量为1的边左侧点向右侧有匹配关系的点连容量为1的边右侧每个点向汇点连容量为1的边。跑一遍最大流最大流的值就是最大匹配数。这种建图方式的正确性在于每条增广路对应一个匹配容量为1保证了每个点最多被匹配一次。相比匈牙利算法Dinic在这种单位容量网络上的复杂度是O(E√V)效率很高。6.2 多源多汇与节点容量限制的处理如果题目中有多个源点或多个汇点可以建立一个超级源点和一个超级汇点分别向所有源点和所有汇点连容量为无穷大的边转化为单源单汇问题。如果题目限制的是节点的通过容量而不只是边的容量可以把每个节点拆成两个点中间连一条容量为节点限制的边。所有原来指向该节点的边连到入点所有从该节点出发的边从出点连出。这个技巧叫做拆点法在网络流建模中非常常用。6.3 最小费用最大流的衔接学习路径掌握了最大流之后最小费用最大流是自然的下一步。它在最大流的基础上给每条边增加了一个单位费用的属性要求在流量最大的前提下费用最小。求解方法通常是把EK或Dinic中的BFS换成最短路算法如SPFA或Dijkstra每次沿着费用最小的增广路推送流量。学习路径建议先彻底搞懂最大流的EK和Dinic再去看最小费用最大流会发现很多概念是相通的。如果最大流没学扎实就直接上费用流很容易一头雾水。我在实际写最大流的代码时最大的体会是模板要背熟但更重要的是理解每一步为什么这么做。比如为什么反向边容量要加delta为什么Dinic要建分层图为什么当前弧优化能提升效率。这些问题想清楚了遇到变形的题目才不会慌。另外建议把EK和Dinic都手敲至少三遍不看任何参考直到能默写出来为止。这个过程看似笨但效果是真的好。

相关新闻

混合发电系统协同调度优化:风电、光热与热电联产建模实战

混合发电系统协同调度优化:风电、光热与热电联产建模实战

凌晨两点,值班室里的风功率预测曲线开始往上翻,光热电站的储热罐温度却已经降到了下限附近,而热电联产机组正被热网指令压着电出力——一边是风电想满发,一边是热用户等着供热,白天攒下来的光热余量还没放干净。这个场…

2026/10/13 10:54:56 阅读更多 →
6G显存也能跑ComfyUI反推与提示词直出:本地部署与避坑指南

6G显存也能跑ComfyUI反推与提示词直出:本地部署与避坑指南

最近折腾 ComfyUI 的花样越来越多,有人拿它做修图,有人拿它跑写真,但我被问得最多的问题是:“只有 6G 显存,能本地玩反推和提示词直出吗?”答案是能,而且比你想象中稳。这篇记录基于我反复装了三…

2026/10/13 8:24:32 阅读更多 →
本地AI视频生成全攻略:基于秋叶ComfyUI整合包从零到成片

本地AI视频生成全攻略:基于秋叶ComfyUI整合包从零到成片

最近不少朋友问我AI视频怎么制作,说云端工具不是充值会员就是限制生成次数,加了各种门槛,想正经做一部AI漫剧、AI短剧,成本高得离谱。其实想实现不限次数、无会员、无充值的本地AI视频生成,完全可以在自己电脑上跑通&a…

2026/10/13 8:24:32 阅读更多 →

最新新闻

Windows下MySQL安装配置与卸载全攻略:从版本选择到残留清理

Windows下MySQL安装配置与卸载全攻略:从版本选择到残留清理

如果你也在Windows上折腾过MySQL,应该能理解那种感受:下载页面一堆版本不知道选哪个,安装到一半卡住,装完了服务起不来,或者换版本时残留文件清不干净,重装总报错。这些坑我基本都踩过一遍,花了…

2026/10/13 11:04:15 阅读更多 →
Thingsboard接入Modbus协议的工程实践与稳定架构设计

Thingsboard接入Modbus协议的工程实践与稳定架构设计

1. 项目概述:为什么一个工业协议接入模块值得单独深挖?“Thingsboard modbus协议”——这六个字背后,不是一句轻飘飘的配置说明,而是一条横跨工业现场与云端管理的真实数据链路。我第一次在某智能配电柜远程监控项目里碰上这个组合…

2026/10/13 11:04:15 阅读更多 →
MySQL与Redis缓存一致性实战:从原理到架构选型

MySQL与Redis缓存一致性实战:从原理到架构选型

做后端这几年,我算是把“缓存和数据库数据对不上”这件事踩透了。团队经常半夜被叫醒,一查原因,十有八九是 Redis 里的旧数据还在服务,而 MySQL 里的记录早就改了。这问题听着朴素,真要讲清楚,背后牵扯到读…

2026/10/13 11:04:15 阅读更多 →
电商数据分析实战:从CLV到营收预测的完整链路

电商数据分析实战:从CLV到营收预测的完整链路

这种预测需求,在如今做电商数据分析的人手里,几乎每周都会撞上一次:老板在周会上盯着营收曲线,问"下个月能不能回到峰值""这批流失客户还值不值得砸券召回";运营拿着用户分群报表,却不…

2026/10/13 11:04:15 阅读更多 →
SpringBoot直连Hive:JDBC接入全流程与常见坑详解

SpringBoot直连Hive:JDBC接入全流程与常见坑详解

最近在给一个数据平台做数据接口,需要让SpringBoot服务通过JDBC直连Hive,把数仓里的离线结果表实时捞过来给前端展示。这个说起来好像就是连个数据库,但实际操作中,Hive和MySQL这些传统关系型数据库在驱动、依赖、协议、会话管理上…

2026/10/13 11:04:15 阅读更多 →
Java全栈演唱会售票系统开发与高并发优化实践

Java全栈演唱会售票系统开发与高并发优化实践

1. 项目概述与核心价值演唱会售票系统是连接演出主办方与观众的关键数字化桥梁。这个基于Java技术栈的全栈解决方案,通过SpringBoot/SSM后端Vue前端uniapp多端的组合架构,实现了从票务管理、在线选座到移动端购票的完整闭环。我在实际票务系统开发中发现…

2026/10/13 11:03:11 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 14:48:49 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →