算法学习笔记:网络流#2——EK 求解最大流

算法学习笔记:网络流#2——EK 求解最大流

1. 前言

本篇博文为 EK 算法求解最大流。

在往下看之前,请先确保您已经了解网络流的一些基础定义,包括但不限于网络,流量,源点,汇点,最大流定义。

如果您对上述定义有一部分不了解,可以前往这篇博文查看:算法学习笔记:网络流#1——有关内容+算法导航

2. 例题

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

P.S.:洛谷还有一道加强版最大流,那道题只能使用 HLPP(最高标号预留推进)加上各种卡常优化来写,但是那个不在讨论范围之内。

PP.S.:如果您想知道那道题到底卡常有多疯狂的话,到 那道题 的题解区里面看一下就知道了。

2.1 详解

假设现在来了一张网络和一只猴子。

在这里插入图片描述

(绘图工具:Windows 10 的 Microsoft Whiteboard)

现在这只猴子想要求解这张图的最大流。

于是这只猴子选了下面这一条红色路径,汇点 t t t 增加 1 流量:

在这里插入图片描述

然后这只猴子想要继续增加汇点流量(下称『推流』),选了下面这条路径。结果它发现不能推流了,于是它得出答案为 1,然后去吃水果了。

但是聪明的您一定能够发现,正确答案是 2,如下图所示:

在这里插入图片描述

猴子吃完水果后发现您得到了正确的最大流,很生气,于是它决定采用爆搜形式解决这道题。

解决完后它得意洋洋的走了,但是我们不满足于爆搜啊,肯定要找到一种更加优秀的写法。

先反观一下上述解法,您会发现猴子错误的原因:做事太快了,都不带思考。

没办法,程序不能思考,但是我们可以让程序反悔!

什么意思呢?

比如说还是这张图,还是这条路径,但是我们动一点手脚:

在这里插入图片描述

发现什么了吗?没错,这张图上出现了反向边。

反向边有什么用呢?

比如说还是这只猴子,在获得反向边加持之后,还想走下面这条路径。

于是它将 s s s 2 2 2 运输了 1 流量之后,发现正向边没得走了,但是存在一条反向边 ( 2 , 1 ) (2,1) (2,1)。于是它就走了,然后得到了正确结果 2。

您可以这样理解反向边的作用:

比如现在有 I N F INF INF 个人要从 s s s t t t,边的流量表示这条路上当前还有几张火车票。假设他们只能坐火车。

于是在 1 个人走了 s − > 1 − > 2 − > t s->1->2->t s>1>2>t 之后,又有一个人想走 s − > 2 − > t s->2->t s>2>t

结果他发现走到 2 2 2 之后没票了!这怎么办啊,不行不行。

于是他通过 2 − > 1 2->1 2>1 的反向边发现有一个人是从 1 1 1 来的,而且这个人可以走 1 − > t 1->t 1>t 来到达 t t t

于是他与之前的人协商了一下,那个人退了票,然后走了 1 − > t 1->t 1>t

于是就有两个人可以到达 t t t

也就是说,反向边可以提供反悔功能。

但是注意到上面那句加粗的话了吗?

接下来给出 网络流 中『增广路』的定义:

  • 增广路:如果存在一条路使得 s s s 能够到达 t t t t t t 的流量增加,那么这条路就叫做增广路。

于是上面的做法就是不断寻找增广路,直到找不到为止。

但是这为什么是正确的?

2.2 正确性证明

本证明同时证明了最大流=最小割。

证明如下:

考虑割掉一些边,使得源点与汇点不连通且点集个数为 2。

s s s 所在的点集为 S S S t t t 所在的点集为 T T T

显然,进入 t t t 的总流量是由 T T T 内的点给的,而流出 s s s 的流量经过被割掉的边进入了 T T T

于是我们有这样一个式子:

  • 原先 S S S T T T 的流量 = 网络总流量

显然,无论怎样构造割,都有这个结果。

那么假设我们知道最大流为 F l o w Flow Flow

  • 在任意构造的割 S S S T T T 中,必有:原先 S S S T T T 的流量 ≤ F l o w \leq Flow Flow

那么现在,在经过上述过程之后,网络没有增广路了。

令当前的网络叫做『残量网络』,那么在残量网络中,令 S S S 表示 s s s 能够到达的所有点, T T T S S S V V V 中的补集, V V V 为点集。

那么,在残量网络中 S S S T T T 流动的流量 = 原先这些被割断的边的容量。

根据上面所证明的,有:网络总流量 = 原先这些被割断的边的流量。

考虑到所有割流量 ≤ F l o w \leq Flow Flow,此时达到的不等式的交界处。

因此,此时得到的网络总流量必为最大流,同时证明了 最小割 = 最大流,证毕。

2.3 做法与代码

实现就很关键了。

在寻找增广路的时候,有两种算法:DFS 与 BFS。

选择哪种算法,将会直接导致程序是 TLE 还是 AC。

为什么这么说呢?

先看看 DFS。

如果采用 DFS 来实现上述不断寻找增广路的过程,叫做 FF 算法。

大体思路就是不断增广,寻找增广路,没有了就结束算法。

很遗憾的是,FF 算法被卡掉了,几乎跟暴力没啥两样。

为什么?究其原因,就是因为 DFS 的效率太低了!

首先考虑每一次我们只能找出一条增广路,DFS 的复杂度至少 O ( n ) O(n) O(n),如果绕的路足够长或者有环,FF 就会彻底 TLE,其实就相当于是跟猴子一样不断暴力,

那么 BFS 呢?

在学搜索的时候各位应该都听过这么一句话:同等状态下,BFS 比 DFS 要优。

如果采用 BFS 写法,大体思路仍然是不断寻找增广路,但是因为 BFS 不会陷入环的陷阱,效率就高了很多。

而这个算法,就叫做 EK 算法。

如果思路理解了,代码应该也是好理解的吧qwq

关于存边与建反向边:

需要注意的是,边的编号要从 2 开始。

为什么呢?这样可以巧妙利用异或的性质:偶数异或 1 就是加一,奇数异或 1 就是减 1。

采取从 2 开始,那么第一组边的编号是 2,3,第二组边是 4,5。

于是在取反向边和改变边权的时候就很方便了。

还有,强烈推荐使用链式前向星,因为 vector 存图在网络流里面简直是太!难!写!了!

代码:

/*
========= Plozia =========
	Author:Plozia
	Problem:P3376 【模板】网络最大流——EK 写法
	Date:2021/3/18
========= Plozia =========
*/

#include <bits/stdc++.h>
using std::queue;

typedef long long LL;
const int MAXN = 200 + 10, MAXM = 5000 + 10;
LL INF = 0x7f7f7f7f7f7f7f7f;
int n, m, s, t, cnt_Edge = 1, Head[MAXN], pre[MAXN], vis[MAXN];
//pre:走过来的边的编号 vis:有没有访问过,防止重复走增广路
LL dis[MAXN], ans;//dis:当前点的流量
struct node
{
    
    
	int to; LL val; int Next;
}Edge[MAXM << 1];//注意 * 2

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;
}
void add_Edge(int x, int y, int z) {
    
    Edge[++cnt_Edge] = (node){
    
    y, (LL)z, Head[x]}; Head[x] = cnt_Edge;}
LL Min(LL fir, LL sec) {
    
    return (fir < sec) ? fir : sec;}

bool bfs()
{
    
    
	queue <int> q; q.push(s);
	memset(vis, 0, sizeof(vis));//清空
	vis[s] = 1; dis[s] = INF;//初始点流量为无穷大
	while (!q.empty())
	{
    
    
		int x = q.front(); q.pop();
		for (int i = Head[x]; i; i = Edge[i].Next)
		{
    
    
			int u = Edge[i].to;
			if (Edge[i].val <= 0) continue;//只考虑大于 0 的边
			if (vis[u] == 1) continue;//不重复走增广路
			pre[u] = i; vis[u] = 1;//标记
			dis[u] = Min(dis[x], Edge[i].val);//推流
			q.push(u);
			if (u == t) return 1;//找到增广路
		}
	}
	return 0;//没有增广路了
}

void update()
{
    
    
	for (int i = t; i != s;)
	{
    
    
		int v = pre[i];
		Edge[v].val -= dis[t]; Edge[v ^ 1].val += dis[t];
		i = Edge[v ^ 1].to;
	}
	ans += dis[t];//更新增广路上的边权
}

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);
	}
	while (bfs()) {
    
    update();}//不断找增广路
	printf("%lld\n", ans);
	return 0;
}

3. 总结

EK 求解最大流的核心思路就是利用 BFS 不断寻找增广路,来更新一路上的边权。

当然,EK 比较容易被卡,所以接下来将会介绍一种基于 FF 思路的更强的算法:dinic 算法(注意不是 EK)。

具体见后面的博文吧。

猜你喜欢

转载自blog.csdn.net/BWzhuzehao/article/details/114979953