最大流的 Dinic 算法和 ISAP 算法
上期回顾:https://www.cnblogs.com/ofnoname/p/18678895
之前我们已经介绍了最大流问题的基本定义、最大流最小割定理、增广路径与残量网络的构建方法,以及如何利用这些概念实现 EK 算法。EK 算法通过每次使用 BFS 寻找从源点到汇点的最短增广路径,保证了算法在有限步内终止,但是频繁的路径搜索会导致效率不高。
在 EK 的基础上,我们将在接下来的文章中重点探讨如何利用分层思想、多路增广以及当前弧优化等策略来克服 EK 算法的不足,使用 Dinic 和 ISAP 算法进一步提高最大流问题求解的效率。
将 EK 改进为 Dinic
EK 的最显而易见问题就是反复 BFS 的浪费,BFS 总是按照层次来选择路径,而多次时间上相邻的 BFS 很可能遍历过程相同,造成了浪费。既然如此,我们可以按照节点到源点距离预先进行分层,然后在选择路径时,总是按照层号递增的方向选择。
Dinic 算法是在 EK 算法的基础上进行改进的一种最大流算法。它通过预先构造分层图,并在同一分层图上多次寻找增广路径,从而大幅提升了算法的效率。下面我们从分层图的构造、阻塞流的寻找以及多次增广这几个方面来详细介绍 Dinic 算法。
首先,Dinic 算法的每一轮使用 $ \text{BFS} $ 从源点 $ s $ 开始,对整个网络进行分层。每个节点都被赋予一个层数,表示它距离 $ s $ 的最短距离。分层的过程中,仅保留那些从一个层次直接通向下一个层次的边,构成所谓的分层图。这样一来,每条边都满足如下条件:
\]

在构造完分层图后,算法进入增广阶段。这一阶段的目标和 EK 一样,是不断寻找所谓的阻塞流(即增广流),也就是在当前分层图中找出所有可能的增广路径,每次寻找都使用 level 数组,更加高效。直至无法再找到从 $ s $ 到 $ t $ 的增广路径为止,此时重新进行分层,进入下一轮 BFS。
当我们已经分层完毕要寻找阻塞流时,为了优化常数,可以使用通常使用深度优先搜索($ \text{DFS} $)在分层图中寻找多条增广路径,每次递归时尽可能沿着分层图中合法的边进行搜索,直到到达汇点 $ t $ 或遇到无法继续前进的情况为止。由于使用了同一套 level,每一轮找到了多条路径是不会互相抵消的。
在一次分层图上找到阻塞流后,算法就会更新残量网络,并重新构造新的分层图。这样,每一次分层和阻塞流求解过程都能够在较大程度上利用分层结构的信息,减少无效搜索,并且保证每次分层后网络中至少有一部分边被“饱和”,从而加速了整体的收敛过程。
当前弧优化
在 $ \text{DFS} $ 阶段,要使用当前弧优化保证正确的复杂度 。其作用是用于减少在 DFS 过程中重复探索已经无效的边。为每个节点记录当前正在尝试的边的位置,避免在同一次 DFS 搜索中重复遍历那些已经证明无法贡献增广流的边,从而提高整体搜索效率。
在具体实现中,每个节点会维护一个“当前弧”,指向该节点的出边邻接表中的某个位置。当 DFS 从某个节点开始时,会从当前弧指针所指向的位置开始尝试后续的边。若某条边经过尝试后证明无法带来有效的增广(例如,该边的剩余容量为 0,或者经过该边之后无法到达汇点 $ t $),则当前弧指针向后移动,以便在后续 DFS 调用中不再重复尝试这条边。这样,在一次 DFS 搜索过程中,每个节点只会考察其邻接边一次,显著降低了不必要的递归和搜索开销。

当前弧优化只是记录了在同一次 DFS 搜索中已经尝试过且无效的边。在 DFS 过程中,如果一条边被证明无法贡献增广流,那么在相同的分层图中,由于网络结构不变,该边未来也不可能转变为一条有效边。换句话说,若某个节点的边在当前 DFS 中尝试后失败,那么在后续的递归调用中再次尝试该边也不会带来不同的结果。因此,将当前弧指针后移不会遗漏任何可能的增广路径。
class Graph {
struct Edge {
int v, res, next;
Edge(int v, int res, int next) : v(v), res(res), next(next) {}
};
vector<int> head;
vector<Edge> edges;
int n, m, s, t;
int dfs_dinic(int u, int flow, const vector<int> &dep, vector<int> &curHead) {
if (u == t) return flow;
int rest = flow;
// 应用当前弧优化:当再次来到此节点时,不需要遍历已经过的边
for (int &i = curHead[u]; i != -1 && rest; i = edges[i].next) {
int v = edges[i].v;
if (dep[v] == dep[u] + 1 && edges[i].res > 0) {
int d = dfs_dinic(v, min(rest, edges[i].res), dep, curHead);
edges[i].res -= d;
edges[i^1].res += d;
rest -= d;
}
}
return flow - rest;
}
public:
void addEdge(int u, int v, int cap) {
// 同时添加两侧边,便于残量网络的构建
edges.emplace_back(v, cap, head[u]);
head[u] = edges.size() - 1;
edges.emplace_back(u, 0, head[v]);
head[v] = edges.size() - 1;
}
Graph(int n, int m, int s, int t) : n(n), m(m), s(s), t(t), head(n+1, -1) {
edges.reserve(m * 2);
}
long long dinic() {
long long res = 0;
vector<int> dep(n+1), head_copy(n+1);
for (;;) {
fill(dep.begin(), dep.end(), 0);
queue<int> q;
q.push(s);
dep[s] = 1;
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = head[u]; i != -1; i = edges[i].next) {
int v = edges[i].v;
if (!dep[v] && edges[i].res > 0) {
dep[v] = dep[u] + 1;
q.push(v);
}
}
}
if (dep[t] == 0) break; // 已经无法到达 t
copy(head.begin(), head.end(), head_copy.begin());
res += dfs_dinic(s, INT_MAX, dep, head_copy);
} return res;
}
};
Dinic 的效率
对于 Dinic 算法的时间复杂度,不同情况下的分析也有所不同。大致而言,在一般的有向网络中,Dinic 算法的最坏情况时间复杂度可以认为是
\]
其中 \(n\) 是网络中的节点数,\(m\) 是边数。严格的上界证明较为复杂,大致来说,这个上界主要来源于如下两个方面:
- 每一次构造分层图的过程通常需要 \(O(m)\) 的时间;
- 在最坏情况下,可能需要执行 \(O(n)\) 次分层及对应的 DFS 寻找阻塞流,每次 DFS 在整个网络中可能会遍历多条边,至多花费 \(O(nm)\) 的时间,从而导致整体复杂度达到 \(O(n^2 \, m)\)。
需要注意的是,上述分析给出的只是最坏情况的理论上界,是非常宽松的。在实际应用中,Dinic 算法通常表现得非常优秀,尤其是在随机图或一般实际问题中,由于网络结构的稀疏性和增广路径分布较为均匀,使得分层和增广过程远低于最坏情况所描述的复杂度。事实上,仅有一些特意构造的数据或特殊的网络结构才能使得 Dinic 算法达到理论上的最差性能。
ISAP:只进行一次 BFS
Dinic 算法虽然简化了路径搜索,通过分层图和阻塞流策略提高了增广效率,但其每一轮也还需要重新构造分层图,ISAP 算法在设计上进一步优化了这一过程。
ISAP 算法采用了一种与 Dinic 不同的策略,它只在初始化时构造一次分层信息。惯例上,ISAP 是从汇点 \(t\) 而不是源点出发,反向进行层次划分(当然 Dinic 也可以从汇点划分,但惯例是源点),确定每个节点到汇点的距离或层次编号。这样一来,每个节点拥有一个初始层次值。
局部更新层次与当前弧
在实际增广过程中,当搜索过程中遇到断流(即当前路径无法继续前进)时,ISAP 不需要重新构造整个分层图,而是仅在断流处局部修改该节点的层次编号以及更新对应的当前弧指针。具体来说:
- 当某个节点无法通过其当前弧获得有效的增广流时,意味着分层需要变化。将节点层次更新为其所有可达点中层次最低值再加一,然后重新从源点开始运行。
- 同时,只更新与该节点相关的当前弧信息(重置到开头),确保后续的搜索不会再次尝试已被判定为无效的边。
这种局部修改策略大大减少了全局分层所需的时间开销,从而显著提高了算法整体的效率。

与 Dinic 算法在同一分层图上寻找阻塞流(即多路增广)不同,ISAP 算法采用与 EK 类似的一次寻找一条增广路径的策略,而不是在同一分层图上多路增广。这种设计保证了在每次断流后,算法能够迅速恢复有效的搜索状态,同时确保层次信息的正确性和路径选择的合理性。最终,ISAP 在很多实际场景中表现出了比 Dinic 更高的效率,尤其是在处理那些非最坏情况的大规模网络时,其局部更新机制能够充分利用初始分层信息,减少不必要的重复计算。其实现反而更加简洁。
GAP 优化
在 ISAP 算法中,GAP 优化是另一项关键的改进技术,用以进一步减少搜索空间,从而提高算法效率。该优化利用了已有层次编号的一个重要性质:如果在层次编号中出现“空档”(即某个层次上没有任何节点),那么所有层次高于该空档的节点都无法通过任何增广路径到达汇点 \(t\)。
具体来说,假设在执行过程中发现层次编号为 \(d\) 的节点数量为 0,即不存在任何节点满足距离汇点为 \(d\)。这时我们可以得出结论: 对于所有距离标签大于 \(d\) 的节点,它们必定无法到达汇点,因此这些节点上不存在任何有效的增广路径。这就意味着全图断流,答案已经得到了。利用这一信息,ISAP 算法便可以立即对这些节点进行剪枝处理。
class Graph {
//......
long long isap() {
long long res = 0;
vector<int> dep(n+1, n), gap(n+1, 0), curHead(head), path(n+1, -1);
queue<int> q;
q.push(t);
dep[t] = 0;
while (!q.empty()) {
int u = q.front();
q.pop();
gap[dep[u]]++;
for (int i = head[u]; i != -1; i = edges[i].next) {
int v = edges[i].v;
if (dep[v] == n && edges[i^1].res > 0) {
dep[v] = dep[u] + 1;
q.push(v);
}
}
}
// 当源点深度标号小于 n 时,说明存在增广路
int u = s;
while (dep[s] < n) {
if (u == t) {
int aug = INT_MAX;
for (int v = t; v != s; v = edges[path[v]^1].v) {
aug = min(aug, edges[path[v]].res);
}
for (int v = t; v != s; v = edges[path[v]^1].v) {
edges[path[v]].res -= aug;
edges[path[v]^1].res += aug;
}
res += aug;
u = s;
continue;
}
bool advanced = false;
for (int &i = curHead[u]; i != -1; i = edges[i].next) {
int v = edges[i].v;
if (edges[i].res > 0 && dep[u] == dep[v] + 1) {
advanced = true;
path[v] = i;
u = v;
break;
}
}
if (!advanced) {
int minDep = n - 1;
for (int i = head[u]; i != -1; i = edges[i].next) {
int v = edges[i].v;
if (edges[i].res > 0) {
minDep = min(minDep, dep[v]);
}
}
if (--gap[dep[u]] == 0) break; // GAP 优化
dep[u] = minDep + 1; // 修改这一点的 level 和当前弧
gap[dep[u]]++;
curHead[u] = head[u];
if (u != s) u = edges[path[u]^1].v;
}
} return res;
}
};
ISAP 的效率
ISAP 算法在最大流问题中的效率非常优秀,尤其是在大多数实际应用中。尽管按照上面的理论,它的时间复杂度上界与 Dinic 算法都是 \(O(n^2 m)\),但 ISAP 算法通过巧妙的局部更新和 GAP 优化以及较小运行常数,在实际应用中,它往往远比 Dinic 更为高效。
最大流的 Dinic 算法和 ISAP 算法的更多相关文章
- 使用Apriori算法和FP-growth算法进行关联分析
系列文章:<机器学习实战>学习笔记 最近看了<机器学习实战>中的第11章(使用Apriori算法进行关联分析)和第12章(使用FP-growth算法来高效发现频繁项集).正如章 ...
- 最小生成树---Prim算法和Kruskal算法
Prim算法 1.概览 普里姆算法(Prim算法),图论中的一种算法,可在加权连通图里搜索最小生成树.意即由此算法搜索到的边子集所构成的树中,不但包括了连通图里的所有顶点(英语:Vertex (gra ...
- mahout中kmeans算法和Canopy算法实现原理
本文讲一下mahout中kmeans算法和Canopy算法实现原理. 一. Kmeans是一个很经典的聚类算法,我想大家都非常熟悉.虽然算法较为简单,在实际应用中却可以有不错的效果:其算法原理也决定了 ...
- 转载:最小生成树-Prim算法和Kruskal算法
本文摘自:http://www.cnblogs.com/biyeymyhjob/archive/2012/07/30/2615542.html 最小生成树-Prim算法和Kruskal算法 Prim算 ...
- 0-1背包的动态规划算法,部分背包的贪心算法和DP算法------算法导论
一.问题描述 0-1背包问题,部分背包问题.分别实现0-1背包的DP算法,部分背包的贪心算法和DP算法. 二.算法原理 (1)0-1背包的DP算法 0-1背包问题:有n件物品和一个容量为W的背包.第i ...
- 用Spark学习FP Tree算法和PrefixSpan算法
在FP Tree算法原理总结和PrefixSpan算法原理总结中,我们对FP Tree和PrefixSpan这两种关联算法的原理做了总结,这里就从实践的角度介绍如何使用这两个算法.由于scikit-l ...
- 字符串查找算法总结(暴力匹配、KMP 算法、Boyer-Moore 算法和 Sunday 算法)
字符串匹配是字符串的一种基本操作:给定一个长度为 M 的文本和一个长度为 N 的模式串,在文本中找到一个和该模式相符的子字符串,并返回该字字符串在文本中的位置. KMP 算法,全称是 Knuth-Mo ...
- 最小生成树之Prim算法和Kruskal算法
最小生成树算法 一个连通图可能有多棵生成树,而最小生成树是一副连通加权无向图中一颗权值最小的生成树,它可以根据Prim算法和Kruskal算法得出,这两个算法分别从点和边的角度来解决. Prim算法 ...
- java实现最小生成树的prim算法和kruskal算法
在边赋权图中,权值总和最小的生成树称为最小生成树.构造最小生成树有两种算法,分别是prim算法和kruskal算法.在边赋权图中,如下图所示: 在上述赋权图中,可以看到图的顶点编号和顶点之间邻接边的权 ...
- Algorithm --> Kruskal算法和Prim算法
最小生成树之Kruskal算法和Prim算法 Kruskal多用于稀疏图,prim多用于稠密图. 根据图的深度优先遍历和广度优先遍历,可以用最少的边连接所有的顶点,而且不会形成回路.这种连接所有顶点并 ...
随机推荐
- php xattr操作文件扩展属性后续
由于之前看了xattr的写入效率,这里简单的实现一下生产者消费者模型的高速写入. 生产者(让他创建40万条数据) <?php // 生产者 不断的生产大量数据 但是总会有停止的时候(本业务功能结 ...
- 符合ASTM标准的雨流计数法及其不同的改进方法
随着研究的深入,人们发现采用时间序列计算载荷谱太麻烦了,处理的工作量太大,我们不需要将每个时刻点的载荷都做运算,疲劳计算只需要提供幅值.均值和循环次数,鉴于此发展出了很多不同的计数方法,雨流法是最常见 ...
- Contrastive Learning 对比学习 | RL 学 representation 时的对比学习
记录一下读的三篇相关文章. 01. Representation Learning with Contrastive Predictive Coding arxiv:https://arxiv.org ...
- Django admin实现图片上传到腾讯云
官网参考:https://docs.djangoproject.com/zh-hans/3.2/howto/custom-file-storage/ 当前业务需要使用django的admin后台进行数 ...
- 13TB的StarRocks大数据库迁移过程
公司有一套StarRocks的大数据库在大股东的腾讯云环境中,通过腾讯云的对等连接打通,通过dolphinscheduler调度datax离线抽取数据和SQL计算汇总,还有在大股东的特有的Flink集 ...
- uniapp云数据库笔记
1.基本概念 云数据库:一个云空间只能有一个数据库,一个数据库可以有多个集合(表),每个表可以有多行数据(文档) DB Schema:是基于 JSON 格式定义的数据结构的规范,每个表有多少字段都需要 ...
- openEuler欧拉设置git pull免密
使用git config命令在本地全局设置用户名和邮箱 git config --global user.name "username":全局添加用户名 git config -- ...
- WinForm 通用权限框架,简单实用支持二次开发
前言 开发一个安全.灵活且易于维护的应用程序是至关重要的.特别是在企业级应用中,权限管理不仅涉及到用户访问控制,还关系到数据的安全性和系统的稳定性. 推荐一款 WinForm 通用.完整的权限架构开发 ...
- 逍遥模拟器+Fiddler抓包 (附带软件)
获取软件地址: 链接:https://pan.baidu.com/s/1zE9AECWOZlw_VDVMAnkBhQ?pwd=c0kq 提取码:c0kq 一.逍遥模拟器安装 1. 安装逍遥模拟器: 2 ...
- web应用模式、API接口、接口测试工具postman、restful规范
目录 一.web 应用模式 二.API接口 API概念 三.接口测试工具postman postman介绍 postman下载与使用 四.如何在浏览器中测试 五.restful规范(重要,不难) 概念 ...