图论专题-网络流-学习笔记:ISAP 求解最大流

1. 前言

本篇博文将会重点讲解 ISAP 求解最大流。

ISAP 求解最大流,是目前笔者知道的 除了 HLPP 之外的速度最快的最大流算法。

在学习 ISAP 求解最大流之前,您需要对以下知识有所了解,包括但不限于:网络流基础定义,FF/EK 求解最大流的 思路,dinic 求解最大流的 代码实现

如果您对上述部分内容不熟悉,可以前往笔者所写的以下博文查看:

  1. 网络流基础定义:图论专题-网络流-学习笔记:网络流基础
  2. FF/EK 求解最大流的 思路图论专题-网络流-学习笔记:EK 求解最大流
  3. dinic 求解最大流的 代码实现图论专题-网络流-学习笔记:dinic 求解最大流

2. 模板

模板题:P3376 【模板】网络最大流

2.1 详解

dinic 算法已经足够高效了,但是非常遗憾,dinic 仍然可以被卡掉,而且出现这种数据就代表 dinic 一定会 TLE,可以见最后面的数据比较。

不行,不稳定的算法我们不要,我们需要一种更加稳定的算法来处理最大流问题。

于是 ISAP 出现了。

ISAP 的核心思路就是:先用 一遍 BFS 从 t 开始 分层,同时记录 gapi 数组表示当前层数为 i 的点的数量,然后寻找增广路。

寻找增广路的过程中,一旦有一个点接受到的流量不能全部流出,那么就将这个点的层数提高 1,如果在提高完之后出现了断层(有一层没有点了),意味着算法结束。

大体分为 3 个步骤:

  1. 分层
  2. 找增广路以及提高层数
  3. 出现断层则结束算法

算法好理解,但是正确性呢?

2.2 正确性证明

ISAP 的正确性证明可能需要感性理解一下。

根据上面的算法步骤,当一个点推流推完的时候,这个点的层数不提高。

为什么?因为此时这个点的层数已经足够其推流了,不需要再提高,而且根据 dinic 算法的要求,流量只能在相邻层之间移动

那么假设当前点推流推不出去了,这个时候需要提高层数,但是为什么这样就一定正确了呢?

想一个问题:如果当前的点无法推流了,那么前面的点呢?

是不是也无法推流了呀!那么因此如果我们提高了这个点的层数,前面的点也必须要提高层数(否则无法推流),此时就能够保证至少这个点接受流量不变。

而如果后面的点有层次更高的点,就可以继续推流,存在增广路;如果没有了,都是层次比较低的点,此时满足以下几个条件:

  1. 层次比较低的点推流完毕,无法再推流(即使得到 INF 的流量)。
  2. 由一,不存在增广路,满足算法结束条件。
  3. 于此同时,由于后面的点层次比较低,又推流完毕,没法提高层次,这个时候就会 出现断层,满足算法结束条件。

综上,算法正确性成立。

而 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 代码不适用于这里的对比)
  • 全部不开启 O2 优化
  • 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. 笔者感觉表格里面 n,m 的数据是假的,因为好像 dinic 在这种数据下根本没法在一秒里面跑完,真正的数据或许是 n=104,m=3×104

首先给出原文中的数据表格:

测试数据 性质 1 性质 2 性质 3 性质 4 性质 5 n= m=
1 105 3×105
2 105 3×105
3 105 3×105
4 105 3×105
5 105 3×105
6 105 3×105
7 105 3×105
8 105 3×105
9 105 3×105
10 105 3×105
11 105 3×105
12 105 3×105
13 105 3×105
14 105 3×105
15 105 3×105
16 105 3×105
17 105 3×105
18 105 3×105
19 105 3×105
20 105 3×105
21 105 3×105
22 105 3×105
23 105 3×105
24 105 3×105

笔者备注:方便起见,原文中所有的 × 已经使用空格代替,23,24 两组数据表示数据不是特殊构造,不具有 5 种性质。

  • 性质 1:不会出现环
  • 性质 2:层次数量很少
  • 性质 3:层次数量很大
  • 性质 4:无解
  • 性质 5:答案较小

笔者备注:在原文中,性质一是专门针对 FF 所设计的,也就是说 FF 只要有环就会 TLE,且没环的数据仍然消耗了极高的时间。

评测环境说明:

  • 时间限制为 60s
  • 开启快读,O2 优化。
  • dinic 和 ISAP 使用当前弧优化。
  • 使用 lemon 评测机测试

最后结果如下:

测试数据 EK dinic ISAP
1 0.171s 0.625s 0.265s
2 0.156s 0.562s 0.265s
3 0.625s 0.828s 0.390s
4 0.312s 0.578s 0.328s
5 0.046s 2.468s 0.218s
6 0.078s 5.546s 0.203s
7 0.109s 5.216s 0.328s
8 0.218s 7.812s 0.265s
9 0.375s 1.281s 0.375s
10 0.156s 0.781s 0.187s
11 0.046s 0.312s 0.203s
12 2.703s 0.875s 0.328s
13 0.156s 0.703s 0.203s
14 0.328s 0.500s 0.218s
15 0.171s 0.296s 0.296s
16 0.234s 0.562s 0.296s
17 0.140s 4.687s 0.343s
18 0.031s 2.921s 0.296s
19 0.040s 2.359s 0.312s
20 0.078s 4.656s 0.390s
21 0.312s 0.500s 0.218s
22 0.203s 1.000s 0.234s
23 0.062s 0.343s 0.265s
24 0.281s 1.015s 0.328s
总用时 7.027s 46.428s 6.754s

根据上表,笔者总结如下:

  • dinic 在层次很深的时候会被卡掉。
  • EK 玄学吊打全场,但是被 #12 卡掉了。
  • ISAP 非常稳。

但是应当指出,这些数据只是在特殊构造情况下的特殊数据,并没有一般性,而一般做题时实践证明,EK 是跑不过 dinic 的。

4. 总结

ISAP 的主要思路:一遍 BFS 分层,然后利用断层巧妙寻找增广路。

到目前为止,笔者已经讲完了 EK,dinic,ISAP 求解最大流,接下来将会进入网络流的另外一个分支:费用流。

传送门:

posted @   Plozia  阅读(318)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示