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

  • Post author:
  • Post category:其他




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






I


N


F





个人要从



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






F


l


o


w





  • 在任意构造的割



    S

    S






    S









    T

    T






    T





    中,必有:原先



    S

    S






    S









    T

    T






    T





    的流量



    F

    l

    o

    w

    \leq Flow















    F


    l


    o


    w





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

令当前的网络叫做『残量网络』,那么在残量网络中,令



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















F


l


o


w





,此时达到的不等式的交界处。

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

最小割 = 最大流

,证毕。



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)。

传送门:

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



版权声明:本文为BWzhuzehao原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。