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

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

  • 1. 前言
  • 2. 模板
    • 2.1 详解
    • 2.2 正确性证明
    • 2.3 代码
    • 2.4 优化
  • 3. 总结

1. 前言

本篇博文讲解求解最大流的 dinic 算法。

在学这篇博文之前,请先确保掌握以下知识:

  1. 网络流的一些基础定义,参见:算法学习笔记:网络流#1——有关内容+算法导航
  2. FF 与 EK 求解最大流的思路,参见:算法学习笔记:网络流#2——EK 求解最大流

下面假设读者已经掌握上述内容。

先来回顾 EK 求解最大流的思路:利用 BFS 不断寻找增广路,不断推流,直到找不到增广路为止。

而 FF 求解最大流的思路:利用 DFS 不断寻找增广路,不断推流,直到找不到增广路为止。

上文中作者提到过,FF 求解最短路效率低下的问题就是因为 DFS 复杂度太高了。

但是:

Dfs 的实现方式虽然暂时无法取得好的效果,但我们并不应该就此放弃,Dfs 相对 Bfs 灵活的架构必能给予我们广阔的优化空间。——《信息学奥赛一本通——提高篇》

所以我们要想想如何优化 DFS。

发明 EK 算法的科学家 Dinic 也意识到了这一点,于是他就在 FF 的基础上加以改进,提出了大名鼎鼎的 dinic 算法,也是目前网络流的主流算法之一,甚至有的人这么说:99% 的网络流问题,都可以用 dinic 通过。

没错,FF 是 Dinic 发明的

接下来就开始讲解吧!

2. 模板

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

2.1 详解

首先我们需要知道 DFS 复杂度为什么高。

假设当前网络构成了一个环,而且边的流量都是 I N F INF INF,于是不管这个环里面流进了多少流量,终将无休止的循环。

那么怎样解决这个问题呢?最显然的一个思路就是控制一下循环层数。

当然这样代码太烦了,于是就有另外一个思路:能不能控制一下点的去向呢?

换句话说,能不能控制一下大体流量的走向呢?

这就是 dinic 算法的主要思路。

dinic 算法的第一步,便是对网络分层。

换句话说,从源点开始,标记源点深度为 1,其能到达的点深度为 2,以此类推。

然后,再开始 DFS,规定只能从层数小的流向层数大的。

但是这样仍然有一个问题,看图:

算法学习笔记:网络流#3——dinic 求解最大流_第1张图片

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

在上图中,您会发现:如果 1 号点不幸的选择了 2 号点,那么就要绕一大圈才能到 t t t,但是实际上只需要经过 150 号点就可以了呀!

因此为了防止这种情况,规定:流量只能在相邻两层之间流动。

这样就可以完美避免了这个问题,但是正确性呢?

仍然正确!见后面的正确性证明。

现在先回到这个问题,假设有一个点推流推不出去了,这说明什么?

这说明这个点已经与汇点不在连通了!这个时候,需要将这个点的层数改为 0,节省时间。

2.2 正确性证明

证明如下:

考虑 FF/EK 算法的终止条件是没有增广路,那么 dinic 也是没有增广路。

在这样分层之后,如果图中还有增广路,那么在 BFS 分层的时候一定会扩展到这条增广路,而最后的终止条件就是没有增广路。

证毕。

2.3 代码

代码如下:

/*
========= Plozia =========
	Author:Plozia
	Problem:P3376 【模板】网络最大流——dinic 求解
	Date:2021/3/18
========= Plozia =========
*/

#include 
using std::queue;

typedef long long LL;
const int MAXN = 200 + 10, MAXM = 5000 + 10;
const LL INF = 0x7f7f7f7f7f7f7f7f;
int n, m, s, t, Head[MAXN], cnt_Edge = 1, dep[MAXN];
struct node {int to; LL val; int Next;} Edge[MAXM << 1];
bool vis[MAXN];
LL ans;
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;}

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;
}

bool bfs()
{
	queue <int> q; q.push(s);
	memset(vis, 0, sizeof(vis));
	memset(dep, 0, sizeof(dep));
	dep[s] = 1; vis[s] = 1;
	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 || vis[u]) continue;//只考虑有流量的点
			vis[u] = 1; dep[u] = dep[x] + 1;
			q.push(u);
		}
	}
	return dep[t];
}

LL dfs(int now, LL Flow)
{
	if (now == t) return Flow;
	LL used = 0;
	for (int i = Head[now]; i; i = Edge[i].Next)//能推流就推流
	{
		int u = Edge[i].to;
		if (Edge[i].val && dep[u] == dep[now] + 1)
		{
			LL Minn = dfs(u, Min(Flow - used, Edge[i].val));
			Edge[i].val -= Minn; Edge[i ^ 1].val += Minn; used += Minn;
			if (used == Flow) return used;
		}
	}
	if (used == 0) dep[now] = 0;//修改层数
	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);
	}
	while (bfs()) ans += dfs(s, INF);//不断找增广路
	printf("%lld\n", ans);
	return 0;
}

2.4 优化

dinic 算法的很重要的一个优化就是当前弧优化。

当前弧优化的意思是这样的:

每一次增广路的时候记录一下当前走到哪一条边(弧)了,下一次到这个点推流的时候直接从这条边开始遍历。

为什么是正确的呢?

显然,已经遍历过的边肯定已经推完了所有流量,也就是再推流也没有用,只有当前这条边可能可以再推流,那么从这条边开始遍历即可。

当前弧优化的代码如下:

/*
========= Plozia =========
	Author:Plozia
	Problem:P3376 【模板】网络最大流——dinic 求解
	Date:2021/3/18
========= Plozia =========
*/

#include 
using std::queue;

typedef long long LL;
const int MAXN = 200 + 10, MAXM = 5000 + 10;
const LL INF = 0x7f7f7f7f7f7f7f7f;
int n, m, s, t, Head[MAXN], cnt_Edge = 1, dep[MAXN], cur[MAXN];
struct node {int to; LL val; int Next;} Edge[MAXM << 1];
bool vis[MAXN];
LL ans;
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;}

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;
}

bool bfs()
{
	queue <int> q; q.push(s);
	memset(vis, 0, sizeof(vis));
	memset(dep, 0, sizeof(dep));
	dep[s] = 1; vis[s] = 1;
	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 || vis[u]) continue;//只考虑有流量的点
			vis[u] = 1; dep[u] = dep[x] + 1;
			q.push(u);
		}
	}
	return dep[t];
}

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[u] == dep[now] + 1)
		{
			LL Minn = dfs(u, Min(Flow - used, Edge[i].val));
			Edge[i].val -= Minn; Edge[i ^ 1].val += Minn; used += Minn;
			if (used == Flow) return used;
		}
	}
	if (used == 0) dep[now] = 0;//修改层数
	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);
	}
	while (bfs()) {for (int i = 1; i <= n; ++i) cur[i] = Head[i]; ans += dfs(s, INF);}//不断找增广路
	printf("%lld\n", ans);
	return 0;
}

3. 总结

dinic 算法的基本步骤:利用 BFS 分层,然后利用 DFS 不断寻找增广路,能推流就推流。

实际上,dinic 算法已经足够高效了,但是很遗憾的是 dinic 在特殊构造的数据下仍然会被卡掉,这个时候就需要一种更高效的算法:ISAP 出场了!

传送门:算法学习笔记:网络流#4——ISAP 求解最大流

你可能感兴趣的:(图论,学习笔记,+,专项训练)