图论专题-网络流-学习笔记:ISAP 求解最大流
1. 前言
本篇博文将会重点讲解 ISAP 求解最大流。
ISAP 求解最大流,是目前笔者知道的 除了 HLPP 之外的速度最快的最大流算法。
在学习 ISAP 求解最大流之前,您需要对以下知识有所了解,包括但不限于:网络流基础定义,FF/EK 求解最大流的 思路,dinic 求解最大流的 代码实现。
如果您对上述部分内容不熟悉,可以前往笔者所写的以下博文查看:
- 网络流基础定义:图论专题-网络流-学习笔记:网络流基础
- FF/EK 求解最大流的 思路:图论专题-网络流-学习笔记:EK 求解最大流
- dinic 求解最大流的 代码实现:图论专题-网络流-学习笔记:dinic 求解最大流
2. 模板
模板题:P3376 【模板】网络最大流
2.1 详解
dinic 算法已经足够高效了,但是非常遗憾,dinic 仍然可以被卡掉,而且出现这种数据就代表 dinic 一定会 TLE,可以见最后面的数据比较。
不行,不稳定的算法我们不要,我们需要一种更加稳定的算法来处理最大流问题。
于是 ISAP 出现了。
ISAP 的核心思路就是:先用 一遍 BFS 从 开始 分层,同时记录 数组表示当前层数为 的点的数量,然后寻找增广路。
寻找增广路的过程中,一旦有一个点接受到的流量不能全部流出,那么就将这个点的层数提高 1,如果在提高完之后出现了断层(有一层没有点了),意味着算法结束。
大体分为 3 个步骤:
- 分层
- 找增广路以及提高层数
- 出现断层则结束算法
算法好理解,但是正确性呢?
2.2 正确性证明
ISAP 的正确性证明可能需要感性理解一下。
根据上面的算法步骤,当一个点推流推完的时候,这个点的层数不提高。
为什么?因为此时这个点的层数已经足够其推流了,不需要再提高,而且根据 dinic 算法的要求,流量只能在相邻层之间移动。
那么假设当前点推流推不出去了,这个时候需要提高层数,但是为什么这样就一定正确了呢?
想一个问题:如果当前的点无法推流了,那么前面的点呢?
是不是也无法推流了呀!那么因此如果我们提高了这个点的层数,前面的点也必须要提高层数(否则无法推流),此时就能够保证至少这个点接受流量不变。
而如果后面的点有层次更高的点,就可以继续推流,存在增广路;如果没有了,都是层次比较低的点,此时满足以下几个条件:
- 层次比较低的点推流完毕,无法再推流(即使得到 的流量)。
- 由一,不存在增广路,满足算法结束条件。
- 于此同时,由于后面的点层次比较低,又推流完毕,没法提高层次,这个时候就会 出现断层,满足算法结束条件。
综上,算法正确性成立。
而 ISAP 巧妙的地方就在于它只要做一遍 BFS,大大减少运行时间,而且因为出现断层就是算法结束,可以保证不会被层次大的构造的图卡掉(具体见后面的对比)。
同样的,ISAP 可以使用当前弧优化,参照 dinic 的当前弧优化,证明也是一样的。
2.3 代码
代码:
/*
========= Plozia =========
Author:Plozia
Problem:P3376 【模板】网络最大流——ISAP 写法
Date:2021/3/19
========= Plozia =========
*/
#include <bits/stdc++.h>
typedef long long LL;
const int MAXN = 200 + 10, MAXM = 5000 + 10;
const LL INF = 0x7f7f7f7f7f7f7f7f;
int n, m, s, t, dep[MAXN], gap[MAXN], cnt_Edge = 1, Head[MAXM << 1], cur[MAXN];
struct node {int to; LL val; int Next;} Edge[MAXM << 1];
int q[MAXN], l, r;
LL ans;
int read()
{
int sum = 0, fh = 1; char ch= getchar();
for (; ch < '0' || ch > '9'; ch = getchar()) fh -= (ch == '-') << 1;
for (; ch >= '0' && ch <= '9'; ch = getchar()) sum = (sum << 3) + (sum << 1) + (ch ^ 48);
return (fh == 1) ? sum : -sum;
}
LL Min(LL fir, LL sec) {return (fir < sec) ? fir : sec;}
void add_Edge(int x, int y, int z) {Edge[++cnt_Edge] = (node){y, (LL)z, Head[x]}; Head[x] = cnt_Edge;}
void bfs()
{
q[l = r = 1] = t;
memset(dep, -1, sizeof(dep)); dep[t] = 0; ++gap[0];
//注意这地方 dep 初始化为 0 会出现一些奇奇怪怪的问题
while (l <= r)
{
int x = q[l++];
for (int i = Head[x]; i; i = Edge[i].Next)
{
int u = Edge[i].to;
if (dep[u] != -1) continue ;
dep[u] = dep[x] + 1; q[++r] = u; ++gap[dep[u]];
}
}
}
LL dfs(int now, LL Flow)
{
if (now == t) return Flow;
LL used = 0;
for (int i = cur[now]; i; i = Edge[i].Next)
{
cur[now] = i; int u = Edge[i].to;
if (Edge[i].val && dep[now] == dep[u] + 1)//注意控制层数
{
LL Minn = dfs(u, Min(Edge[i].val, Flow - used));
if (Minn)
{
Edge[i].val -= Minn; Edge[i ^ 1].val += Minn; used += Minn;
if (used == Flow) return used;
}
}
}
--gap[dep[now]];//提高层数
if (gap[dep[now]] == 0) dep[s] = n + 1;//出现断层
++dep[now]; ++gap[dep[now]];
return used;
}
int main()
{
n = read(), m = read(), s = read(), t = read();
for (int i = 1; i <= m; ++i)
{
int x = read(), y = read(), z = read();
add_Edge(x, y, z); add_Edge(y, x, 0);
}
bfs();
while (dep[s] <= n) {for (int i = 1; i <= n; ++i) cur[i] = Head[i]; ans += dfs(s, INF);}
//出现断层就结束算法
printf("%lld\n", ans);
return 0;
}
3. 算法对比
3.1 一般数据下的对比
一般数据的对比参照 luogu 的模板题提交结果。
- 全部采用快读
- 全部采用 STL 容器的队列(所以上面的 ISAP 代码不适用于这里的对比)
- 全部不开启 优化
- dinic 和 ISAP 采用当前弧优化
- 全部不对重边进行合并处理,这样做是为了更具有可比性
- 代码长度以 Dev-C++ 数据为准
- 因为码风问题,或许笔者所测的代码长度与读者所测的代码长度差距很大,请读者谅解。
因为 FF 被群殴了所以没有 FF
算法 | 代码长度 | 使用时间 | 使用空间 |
---|---|---|---|
EK | 1.746K | 605ms | 892.00KB |
dinic | 2.071K | 45ms | 884.00KB |
ISAP | 2.078K | 38ms | 900.00KB |
从上面的表格可以看出来:
- EK 的码量相对短一点,但是耗时太长了。
- dinic 和 ISAP 其实在这些数据下没什么差别。
其实一般的网络流题目主要考建模能力,一般不会来卡你算法时间(当然 EK 被卡掉比较普遍),但是万一出题人构造数据卡你,那么还是使用 ISAP 吧。
顺便提一下,根据目前笔者所了解,ISAP 貌似不支持费用流问题,dinic 支持,所以 dinic 还是很重要的。
当然 ISAP 实在是太难卡了(目前笔者无法构造 hack 数据)以至于没必要学 HLPP,HLPP 码量又长又容易写错,而且必须加各种玄学优化才会比 ISAP 快,普通的 HLPP 跑不过 ISAP。
3.2 特殊数据下的对比
此处的数据引用了洛谷用户 @离散小波变换° 在加强版模板 P4722 【模板】最大流 加强版 / 预留推进 题解里面给出的数据,在此表示感谢。
P.S. 笔者感觉表格里面 的数据是假的,因为好像 dinic 在这种数据下根本没法在一秒里面跑完,真正的数据或许是 。
首先给出原文中的数据表格:
测试数据 | 性质 1 | 性质 2 | 性质 3 | 性质 4 | 性质 5 | ||
---|---|---|---|---|---|---|---|
1 | √ | √ | √ | ||||
2 | √ | √ | √ | ||||
3 | √ | √ | |||||
4 | √ | √ | |||||
5 | √ | √ | √ | ||||
6 | √ | √ | √ | ||||
7 | √ | √ | |||||
8 | √ | √ | |||||
9 | √ | √ | |||||
10 | √ | √ | |||||
11 | √ | ||||||
12 | √ | ||||||
13 | √ | √ | |||||
14 | √ | √ | |||||
15 | √ | ||||||
16 | √ | ||||||
17 | √ | √ | |||||
18 | √ | √ | |||||
19 | √ | ||||||
20 | √ | ||||||
21 | √ | ||||||
22 | √ | ||||||
23 | |||||||
24 |
笔者备注:方便起见,原文中所有的 × 已经使用空格代替,23,24 两组数据表示数据不是特殊构造,不具有 5 种性质。
- 性质 1:不会出现环
- 性质 2:层次数量很少
- 性质 3:层次数量很大
- 性质 4:无解
- 性质 5:答案较小
笔者备注:在原文中,性质一是专门针对 FF 所设计的,也就是说 FF 只要有环就会 TLE,且没环的数据仍然消耗了极高的时间。
评测环境说明:
- 时间限制为 。
- 开启快读, 优化。
- dinic 和 ISAP 使用当前弧优化。
- 使用 lemon 评测机测试
最后结果如下:
测试数据 | EK | dinic | ISAP |
---|---|---|---|
1 | |||
2 | |||
3 | |||
4 | |||
5 | |||
6 | |||
7 | |||
8 | |||
9 | |||
10 | |||
11 | |||
12 | |||
13 | |||
14 | |||
15 | |||
16 | |||
17 | |||
18 | |||
19 | |||
20 | |||
21 | |||
22 | |||
23 | |||
24 | |||
总用时 |
根据上表,笔者总结如下:
- dinic 在层次很深的时候会被卡掉。
- EK 玄学吊打全场,但是被 #12 卡掉了。
- ISAP 非常稳。
但是应当指出,这些数据只是在特殊构造情况下的特殊数据,并没有一般性,而一般做题时实践证明,EK 是跑不过 dinic 的。
4. 总结
ISAP 的主要思路:一遍 BFS 分层,然后利用断层巧妙寻找增广路。
到目前为止,笔者已经讲完了 EK,dinic,ISAP 求解最大流,接下来将会进入网络流的另外一个分支:费用流。
传送门:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?