图论专题-网络流-学习笔记: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\) 开始 分层,同时记录 \(gap_i\) 数组表示当前层数为 \(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 代码不适用于这里的对比)
  • 全部不开启 \(O_2\) 优化
  • 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=10^4,m=3 \times 10^4\)

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

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

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

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

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

评测环境说明:

  • 时间限制为 \(60s\)
  • 开启快读,\(O_2\) 优化。
  • 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\) \(\color{red}{2.468s}\) \(0.218s\)
6 \(0.078s\) \(\color{red}{5.546s}\) \(0.203s\)
7 \(0.109s\) \(\color{red}{5.216s}\) \(0.328s\)
8 \(0.218s\) \(\color{red}{7.812s}\) \(0.265s\)
9 \(0.375s\) \(\color{purple}{1.281s}\) \(0.375s\)
10 \(0.156s\) \(0.781s\) \(0.187s\)
11 \(0.046s\) \(0.312s\) \(0.203s\)
12 \(\color{red}{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\) \(\color{red}{4.687s}\) \(0.343s\)
18 \(0.031s\) \(\color{red}{2.921s}\) \(0.296s\)
19 \(0.040s\) \(\color{red}{2.359s}\) \(0.312s\)
20 \(0.078s\) \(\color{red}{4.656s}\) \(0.390s\)
21 \(0.312s\) \(0.500s\) \(0.218s\)
22 \(0.203s\) \(\color{purple}{1.000s}\) \(0.234s\)
23 \(0.062s\) \(0.343s\) \(0.265s\)
24 \(0.281s\) \(\color{purple}{1.015s}\) \(0.328s\)
总用时 \(7.027s\) \(46.428s\) \(6.754s\)

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

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

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

4. 总结

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

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

传送门:

posted @ 2022-04-17 14:49  Plozia  阅读(384)  评论(0)    收藏  举报