网络最大流

2019-02-22 更新

理解网络

一张网络,是一张带权有向图。比喻成输水系统可能好理解——

源头是大水库,想输出多少就输出多少。

但是,想要输出到目的地,需要经过中转点。中转点不产生新流量、也不私吞;接受多少流量,就同时输出多少流量

点与点之间管道的容量(可以理解为水流量限制;注意“流量”指单位时间通过量,可以想象一下)是有限的;一条边上的流量不能超出其容量,容量很可能是有残余的。因为这些边上的限制,源头并不能无限输出。

于是有的人就想要求出:这张网络运作起来的时候,总流量最大能有多少。由于容量限制比较复杂,似乎不容易规划一个最佳方案。

下面来了一张经典图和一只猴子。

边上的数字是其容量限制。

红色的路径,是猴子随便添加的一条可行流(电脑不能看见整张图,所以它跟猴子没啥区别)。这个时候总流量是 (1)

然后猴子继续随机尝试。想要让水流通过下方的黑边,但接下来没法把这份流量传到 (T)。猴子决定吃水果去了。

现在水正在按照红色路径流动,流量只有 (1),就没法操作了。但你凭借聪明才智,很容易发现这是一种糟糕的策略,只要开始的时候 (S) 沿着上下两条路走到 (T),总流量就有 (2) 了。

机智的猴子决定回来暴搜所有流法,它能解决这种规模的数据就已经很满意了……当然我们有更好的方法:

在水顺着管子流过去的时候,添加一条反向边,边的容量等于此次流量,允许其它路径过来。啥玩意儿?

想象红色管子里这个水正在咕咕流动,就这个方向。网络中暗暗显现了它们对应的反向边。

这时候再次尝试找路(S) 顺着下方管道给 (b) 传过去 (1) 流量。然后 (b) 找到了一条反向边流到 (a)?有点颠覆世界观啊。

不必理解成 (b) 流回 (a),而可当作 (a) 的反悔。

为主体进行考虑。(a) 看到源点向 (b) 给予 (1) 流量,于是 (a) 决定收回它给予的等量流量,拿去做别的事情,于是管道被空置了。

“我先前帮助 (b),给他 (1) 流量流到终点去;现在源点跟他关系那么好,支援他 (1) 流量,使他足够维持输出,那我也要拿回那 (1) 流量,不流向他了!”

(其实你不拿回的话,(b) 就不平衡了)

于是,对中间的边,(a) 不进行输出

当然,在算法运行的时候,“某次找到这条路并尝试输出、接下来某次找路时退回”的过程是存在的。退回方向的反向边(即原边)获得了 (1) 份新的残余容量,已经回到最早的状态了。(a) 收回流量后,它决定流向终点,网络又增加了 (1) 流量(原来的流量没有被破坏)。

从源点开始,顺着有流量的边,只要能找到一条能到达汇点的路(不管路径上是否有“退回”操作),就会使网络总流量增加,这种路叫增广路。上面是两次增广,然后就发现网络就没有增广路了,并且成果好像还不错。


最大流的算法

对一张网络,是不是只要不断找增广路(增广过程中记得提供退回操作的机会),直到找不到,就是网络最大流?

抱歉,这是真的。为什么呢?

...无数实践已经证明。

Part 1

一个网络正在按照猴子的计划流动。

于是猴子作了一个割。割是一个边集:这些边如果去掉,就会使图变成两个不相连的点集,一个点集 (S) 包含源点,一个点集 (T) 包含汇点。

原本 (S)(T) 那几条边,它们的净流量(实际流量)称为 (S)(T) 的净流量。注意,可能有水从 (T) 流向 (S),它们对净流量是负的贡献。

对于这张网络,除了源点和汇点,其它点都是“流入多少,就流出多少”,没有实际贡献或收入。

汇点收到的流量,是 (T) 中所有点的综合作用;(T) 中的点本身(除了汇点),既不产生流量也不私藏流量,那么它们从哪里获得给汇点的流量呢?只能是 (S) 传过来的。所以,

  • (S)(T) 的净流量 = 源点输出 = 汇点收入 = 网络总流量

不管怎么构造割,都有这个结果,所以,

  • 图上任何一种割的净流量 = 网络总流量

因为割的净流量不可能超过此割的总容量(当然啦),所以,

  • 网络总流量 <= 图上任何一种割的容量

Part 2

经过猴子的调整,它的网络上没有增广路了。源点顺着残量网络到不了汇点了。

源点顺着残量网络能够到达的点,叫做点集 (S)(至少包含自己),剩下所有不能到达的点,叫做点集 (T)。则 (S) 连向 (T) 的边都没有残余容量(如果有残量边,(S) 也顺着这条边包含了对面的点)。

如果把 (S)(T) 中间的边割断,那么这个割的净流量 = 这个割的容量(因为边上都没有残余容量了)。

又因为 Part 1 证的“网络总流量 = 任意一个割的净流量”,所以网络总流量 = 此割的容量(①)

其实这很不容易做到的,因为 Part 1 证了“网络总流量 <= 图上任何一种割的容量”,所以“①”一定是面这个不等式的交界处。

所以,这个网络流已经是众望所归。

一个总体的印象是:只要没有增广路,即顺着残量网络到不了汇点,就说明存在一个无残量的割,其净流量等于容量;因为网络总流量同时等于图上任何一个割的净流量,也就同时绝不会超过图上任何一个割的容量,所以当总流量 = 某割的容量时,已经没有比它更大的流,分号以前达到了这个条件,所以现在是最大流。

(还顺便证明了最大流 = 最小割)

增广路定理的证明其实很重要,它把网络最大流问题化简为:

不断找增广路就好了

但是考虑到每次流过去,反向边容量又增加了,提供太多的反悔机会,会死循环吗?

不会的。首先明确,最大流是有限的(当然);而每次找增广路,总流量不会减少,只会增加(重申一下,一条边的“收回流量”操作并不会减少流量,毕竟收回的前提是对方获得支援;增加流量本身没有任何代价,只是有各边的容量限制着),在有限次增广后,总流量一定会达到最大值,然后找不到增广路,结束。

实现起来也有很多细节。

#include <cstdio>
#include <cstring>
#include <algorithm>
using std::min;

const int N = 10010, E = 200010;

int n, m, s, t;
int first[N];
int to[E], nxt[E], val[E]/*残余容量*/;
int cnt = 1;
//cnt初值1,第一条边的标号为2(二进制10),第二条是3(二进制11) 
//有啥好处呢?
//我们加入一条边时,紧接着加入它的反向边(初始容量0) 
//这两条边的标号就是二进制最后一位不相同,一个0、一个1
//所以要召唤 p 这条边的反向边,只需用 p ^ 1
//如果cnt初值为0,就做不到。当然初值-1也可以,略需改动

//关于图中真正的反向边,可能引起顾虑,应该让它们标号相邻? 
//其实不用。该找到的增广路都会找到的 

bool vis[N];//限制增广路不要重复走点,否则很容易爆栈 
//兜一大圈走到汇点,还不如直接走到汇点

void addE(int u, int v, int w) {
	++cnt;
	to[cnt] = v;
	val[cnt] = w;
	nxt[cnt] = first[u];
	first[u] = cnt;
}
int dfs(int u, int flow) {
	//注意,在走到汇点之前,无法得知这次的流量到底有多少 
	if (u == t)
		return flow;//走到汇点才return一个实实在在的流量 
	
	vis[u] = true;
	for (int p = first[u]; p; p = nxt[p]) {
		int v = to[p];
		if (val[p] == 0 or vis[v])//无残量,走了也没用 
			continue;
		int res = 0;
		if ((res = dfs(v, min(flow, val[p]))) > 0) {
			//↑顺着流过去,要受一路上最小容量的限制
			val[p] -= res;//此边残余容量减小
			val[p ^ 1] += res;//以后可以顺着反向边收回这些容量,前提是对方有人了 
			return res;
		}
	}
	return 0;//我与终点根本不连通(依照残量网络),上一个点不要信任我
}
int main() {
	scanf("%d %d %d %d", &n, &m, &s, &t);
	for (int i = 1; i <= m; ++i) {
		int u, v, w;
		scanf("%d %d %d", &u, &v, &w);
		addE(u, v, w);
		addE(v, u, 0);//和正向边标号相邻
		//反向边开始容量为0,表示不允许平白无故走反向边
		//只有正向边流量过来以后,才提供返还流量的机会
	}
	int res = 0, tot = 0;
	while (memset(vis, 0, sizeof(vis)) and (res = dfs(s, 2e9/*水库很强*/)) > 0)
		tot += res;//进行若干回合的增广

	printf("%d
", tot);
	return 0;
}

这种直接深搜找增广路的办法叫做 Ford-Fulkerson(FF)算法。

由于每次只找一条路,这条路还可能绕远路(可能经过 n 个点才到达汇点),而且增加流量是路上最小的权值,效率低,好像很容易被卡掉。但上面的代码能是通过模板题的,最慢一个点 500ms。

不过你可以看出,这个算法的复杂度和流量有关,令人担心。

Maximum Flow Faster Algorithm

我们还是有办法解决 FF 效率低的问题。

  • 每次多路增广:u 点通过一条边,向 v 输出流量以后,v 会尝试到达汇点(到达汇点才真正增广),然后 v 返回实际增广量。这时,如果 u 还有没用完的供给,就继续尝试输出到其它边。

    但是要警惕绕远路、甚至绕回的情况,不加管制的话极易发生。怎么管?

  • 源点顺着残量网络想要到达其它点,需要经过一些边对吧?按照经过的边数(即源点出发以后的距离)把图分层,即用 bfs 分层。每次尝试给予时,只考虑给予自己下一层的点,就可以防止混乱。

  • 综合上面两条。每回合也是从源点出发,先按照当前残量网络分一次层,随后多路增广,尽可能增加流量。增广过程中,会加入一些反向边,这些反向边逆着层次图,本回合并不会走。所以还需要进入下一回合。一直到 bfs 分层时搜不到汇点(即残量网络断了)为止。

这是 Dinic 算法。如果懂 FF,这个算法也很块能懂。

可是它每次只按照 bfs 分层的固定方向进行增广,还能保证正确性吗?这个好理解。只要图中还有增广路(源点顺着残量网络到达汇点的路),bfs 分层就会搜索到汇点,于是增广就不会停止,最终也止于没有增广路的局面。

虽然从它“每次多路增广、绕弯少”能够大致体会它比 FF 快,但它保证会快吗?

...无数实践也已经证明。

要是把 Dinic 卡到上界的话,模板题都过不去了。

#include <cstdio>
#include <cstring>
#include <algorithm>
using std::min;

const int N = 10010, E = 200010;

int n, m, s, t, ans = 0;
int cnt = 1, first[N], nxt[E], to[E], val[E];
inline void addE(int u, int v, int w) {
	to[++cnt] = v;
	val[cnt] = w;
	nxt[cnt] = first[u];
	first[u] = cnt;
}
int dep[N], q[N], l, r;
bool bfs() {//顺着残量网络,方法是 bfs;这是个bool型函数,返回是否搜到了汇点 
	memset(dep, 0, (n + 1) * sizeof(int));//记得开局先初始化 

	q[l = r = 1] = s;
	dep[s] = 1;
	while(l <= r) {
		int u = q[l++];
		for(int p = first[u]; p; p = nxt[p]) {
			int v = to[p];
			if(val[p] and !dep[v]) {//按照有残量的边搜过去 
				dep[v] = dep[u] + 1;
				q[++r] = v;
			}
		}
	}
	return dep[t];//dep[t] != 0,就是搜到了汇点 
}
int dfs(int u, int in/*u收到的支持(不一定能真正用掉)*/) {
//注意,return 的是真正输出的流量
	if(u == t)
		return in;//到达汇点是第一个有效return 
	int out = 0;
	for(int p = first[u]; p and in; p = nxt[p]) {
		int v = to[p];
		if(val[p] and dep[v] == dep[u] + 1) {//仅允许流向下一层 
			int res = dfs(v, min(val[p], in)/*受一路上最小流量限制*/);
			//res是v真正输出到汇点的流量
			val[p] -= res;
			val[p ^ 1] += res;
			in -= res;
			out += res;
		}
	}
	if(out == 0)//我与终点(顺着残量网络)不连通 
		dep[u] = 0;//上一层的点请别再信任我,别试着给我流量
	return out;
}
int main() {
	scanf("%d %d %d %d", &n, &m, &s, &t);
	for(int i = 1; i <= m; ++i) {
		int u, v, w;
		scanf("%d %d %d", &u, &v, &w);
		addE(u, v, w);
		addE(v, u, 0);
	}
	while(bfs()) 
		ans += dfs(s, 2e9);
	printf("%d
", ans);
	return 0;
}

注意理解多路增广:虽然一个点要枚举所有出边,但实质仍然是 dfs,过程图类似于树。

复杂度可能的证明:《浅谈基于分层思想的网络流算法》 P4 ~ P14。但是这里关于增广次数的证明中,某个结论好像忽略了一种情况。

Dinic 能应付大多数网络流题目了,大概因为网络流题目主要考察建模能力吧。当然最大流还有更高效的算法。


更新日志:

2019-02-22 代码和注释改动。

原文地址:https://www.cnblogs.com/cicos/p/10258708.html