本篇博文讲解求解最大流的 dinic 算法。
在学这篇博文之前,请先确保掌握以下知识:
下面假设读者已经掌握上述内容。
先来回顾 EK 求解最大流的思路:利用 BFS 不断寻找增广路,不断推流,直到找不到增广路为止。
而 FF 求解最大流的思路:利用 DFS 不断寻找增广路,不断推流,直到找不到增广路为止。
上文中作者提到过,FF 求解最短路效率低下的问题就是因为 DFS 复杂度太高了。
但是:
Dfs 的实现方式虽然暂时无法取得好的效果,但我们并不应该就此放弃,Dfs 相对 Bfs 灵活的架构必能给予我们广阔的优化空间。——《信息学奥赛一本通——提高篇》
所以我们要想想如何优化 DFS。
发明 EK 算法的科学家 Dinic 也意识到了这一点,于是他就在 FF 的基础上加以改进,提出了大名鼎鼎的 dinic 算法,也是目前网络流的主流算法之一,甚至有的人这么说:99% 的网络流问题,都可以用 dinic 通过。
没错,FF 是 Dinic 发明的
接下来就开始讲解吧!
模板题:P3376 【模板】网络最大流
首先我们需要知道 DFS 复杂度为什么高。
假设当前网络构成了一个环,而且边的流量都是 I N F INF INF,于是不管这个环里面流进了多少流量,终将无休止的循环。
那么怎样解决这个问题呢?最显然的一个思路就是控制一下循环层数。
当然这样代码太烦了,于是就有另外一个思路:能不能控制一下点的去向呢?
换句话说,能不能控制一下大体流量的走向呢?
这就是 dinic 算法的主要思路。
dinic 算法的第一步,便是对网络分层。
换句话说,从源点开始,标记源点深度为 1,其能到达的点深度为 2,以此类推。
然后,再开始 DFS,规定只能从层数小的流向层数大的。
但是这样仍然有一个问题,看图:
(绘图工具:Windows 10 的 Microsoft Whiteboard)
在上图中,您会发现:如果 1 号点不幸的选择了 2 号点,那么就要绕一大圈才能到 t t t,但是实际上只需要经过 150 号点就可以了呀!
因此为了防止这种情况,规定:流量只能在相邻两层之间流动。
这样就可以完美避免了这个问题,但是正确性呢?
仍然正确!见后面的正确性证明。
现在先回到这个问题,假设有一个点推流推不出去了,这说明什么?
这说明这个点已经与汇点不在连通了!这个时候,需要将这个点的层数改为 0,节省时间。
证明如下:
考虑 FF/EK 算法的终止条件是没有增广路,那么 dinic 也是没有增广路。
在这样分层之后,如果图中还有增广路,那么在 BFS 分层的时候一定会扩展到这条增广路,而最后的终止条件就是没有增广路。
证毕。
代码如下:
/*
========= 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;
}
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;
}
dinic 算法的基本步骤:利用 BFS 分层,然后利用 DFS 不断寻找增广路,能推流就推流。
实际上,dinic 算法已经足够高效了,但是很遗憾的是 dinic 在特殊构造的数据下仍然会被卡掉,这个时候就需要一种更高效的算法:ISAP 出场了!
传送门:算法学习笔记:网络流#4——ISAP 求解最大流