网络流各算法超详细带源码解析
网络最大流
链接:洛谷日报EK 博客1 博客2 洛谷日报Dinic 洛谷日报ISAP和HLPP
FF算法:朴素的算法
Ford-Fulkerson's Algorithm
【名词】增广路:一条从起点走到终点的道路,其上的剩余流量的最小值大于0,能够为答案做出贡献。
【动词】增广:对一条增广路进行增广,就是求出这条路上的剩余流量最小值Min,然后给这条路上的每一条路减去Min,同时给它们的反向加上Min。
FF算法的原理就是:随机找一条走到从源点S走到终点T的增广路,然后对这条路增广。所以你在走的时候,要判断这条边的剩余流量是否已经耗到了0,耗到0就不走了。
很简单,就是一个dfs就完事了。在dfs中维护路上的最小值,在到达t后回溯的过程中对边进行修改。函数返回值是这条路的最小值,而最终答案则是用全局变量保存,在每次到达t点的时候加进答案。
因为每一次增广都会增加答案,而最大流显然是有限的,所以这样的增广也只会进行有限次——只是会有很多次。真的很多很多。
最经典的反例就是这个。
1 ---> > 2
\ / \
\ / \
>3------->>4
在这个图上用FF算法可能会跑200000次。而且,只要增加边的权值就可以增加你走的次数,而这个权值完全可以增加到\(10^9\)。所以FF几乎是没人用的。
EK算法:BFS优化
Edmond-Karp's Algorithm
EK的算法是在FF的基础上优化的,它的基本思想就是:通过从S出发广搜,优先走最短的路。
每次广搜时记录from
数组和fromp
数组,记录来源的点,和来源的边的编号。当你广搜搜到了T点的时候,就可以立即停手,然后返回。与FF相似地,剩余流量为0的边我也是不会走的。
接着,你就可以从T点出发,沿着from一路回到S点,这就是一条增广路。
当你BFS搜不到T点的时候,就说明已经没有增广路了,那么你就可以返回了。
每一个人都会觉得这有点浪费。“我BFS搜遍了整个图,结果只有一条路能用!”所以,DinIc就产生了。
EK算法的时间是\(O(NM^2)\)。
Dinic算法:记录到S距离
Dinic的广搜多了一点东西:你要保存每一个点到S点的最短距离dep[i]
。(其实相当于层级)
然后,你在dfs的时候就严格地要求只能从u点走到dep
比u大1的点。这样就达到了“增广路是最短路”的目的。优化之处在于,你是可以利用这一次BFS的成果,进行多次dfs的;每次dfs能且只能处理一条最短路(是不是有点像FF算法)。这样BFS的效用就被增大了。
dfs的内容就是在找到t点之后一路返回,一边返回一边修改边的剩余流量。
Dinic算法的时间是\(O(N^2M)\),在稀疏图上和EK差不多,但是到了稠密图上就快很多。
在这个程度上,优化其实就已经很明显了。然而Dinic还可以加两个Buff(优化)。
多路增广:真·DFS
假设你从S点走到当前的u点的路上的剩余流量最小值是Rest,那么你可以在枚举dfs出边的时候,走完一个支线之后再在下一个支线走,直到Rest被耗完或者出边被走完了。
完成这个优化只需要在原dfs中做一些不大的修改。
注意一件事情:虽然是多路增广,但是在一次广搜之后还是要进行多次的深搜的。不能保证一次深搜就能用完一次BFS的成果。判断DFS是否已经足够的方法就是:看DFS能否在那个dep的限制下走到T。(这一段待验证)
当前弧优化:不做无用功
在这个图上,在两次BFS之间的多次dfs中,同一个点u可能可以通过不同的几条路到达。
在以前的深搜中,我已经把前面的几条边的未来可能用的流量用完了,这条边已经成了”废边“,所以就算搜这几条边也只能获得0的收益,而且这些多余的深搜还会蛮耗时间。
所以,我们可以用一个cur数组临时代替tu数组,让下次再来的时候直接从cur开始。cur表示的是离tu最近的还没有增广完的边。
什么时候修改cur呢?最好的方法就是:在v = to[p]
的下面一句就加上cur[u]=p
。
// 在这里整合一下加了两个Buff的Dinic的深搜
int dfs(int u, int low) {
int left = low;
if(u == t) {
flag = true; // 代表成功地走到了T节点,可以继续dfs。如果flag = false,那么就需要重新广搜。
Maxflow += low;
return low;
}
for(int v, p = cur[u]; p; p = nxt[p]) {
v = to[p];
cur[u] = p;
if(dep[v] == dep[u] + 1 && f[p] > 0) { // f[p]就是边的流量
int gone = dfs(v, min(left, f[p]));
f[p] -= gone;
f[p ^ 1] += gone;
left -= gone;
if(left <= 0) break;
}
}
return low - left;
}
补充:Dinic跑二分图匹配比匈牙利算法还快得多。
补充:如果输入数据中一条边的正反两条边都有,那么在这两个点之间就会有4条边。
补充:CSP是不会卡Dinic的。
补充:要用vis。不然会导致在一条边上反复走。
ISAP算法:动态修改分层
Improved Shortest Augumenting Path
闲的没事的科学家们对于Dinic还不满足,发明了ISAP。
先来算法步骤:
-
从t到s跑一遍bfs,标记深度。
-
从s到t跑dfs,和Dinic类似,只是当一个点的所有出边都被耗完后,如果从上一个点传过来的flow比该点的used大(对于该点当前的深度来说,该点在该点以后的路上已经废了),则把它的深度加1。此时判断如果出现断层(某个深度没有点),把源点S的深度标记为n+1,结束算法。
-
如果操作2没有结束算法,重复操作2
原理:
每个点的深度随着dfs的进行而不断提高。当所有边走完后,这个点就成了“废点”。
注意:
- 广搜时没有剩余流量>0的限制。
- 深搜的时候,前面部分与Dinic没有区别,只在函数最后进行修改深度的操作。
- 要用桶来统计并维护每个深度的点的个数,以快速判断是否出现断层。
- ISAP的主函数
void ISAP()
中唯一的循环是while(dep[s] > n) dfs(s, INF);
- 可以使用当前弧优化,但是不知道能不能多路增广。我自己推不出来,高二也没有详细了解。
时间复杂度仍然是\(O(N^2M)\),但是比Dinic快。
预流推进算法
基本思想就是:源点有INF的水,然后往每一个点灌尽量多的水(称为“推流”),一直到最后。思想很简单,但是实际上有很多麻烦的事情。
预留推进算法的思想是:
-
先假装s有无限多的余流,从s向周围点推流(把该点的余流推给周围点,注意:推的流量不能超过边的容量也不能超过该点余流),并让周围点入队。注意:s和t不能入队 。
-
不断地取队首元素,对队首元素推流
-
队列为空时结束算法,t点的余流即为最大流。
上述思路是不是看起来很简单,也感觉是完全正确的?
但是这个思路有一个问题,就是可能会出现两个点不停地来回推流的情况,一直推到TLE。
怎么解决这个问题呢?
给每个点一个高度,水只会从高处往低处流。在算法运行时, 不断地对有余流的点(包括推出去的点和被推流的点)更改高度,改为它推出去了所有点中高度最高的点的高度+1(如果是被推流的点就+1) ,直到这些点全部没有余流为止。
为什么这样就不会出现来回推流的情况了呢?
当两个点开始来回推流时,它们的高度会不断上升,当它们的高度大于s时,会把余流还给s。
所以在开始预流推进前要先把s的高度改为n(点数),免得一开始s周围那些点就急着把余流还给s。
这个预留推进算法相当慢。我们学这个的目的是为下面的东西做铺垫。
HLPP:升级版预流推进
算法步骤:
1.先从t到s反向bfs,使每个点有一个初始高度
2.从s开始向外推流,将有余流的点放入优先队列
3.不断从优先队列里取出高度最高的点进行推流操作
4.若推完还有余流,更新高度标号,重新放入优先队列
5.当优先队列为空时结束算法,最大流即为t的余流
与基础的余流推进相比的优势:
通过bfs预先处理了高度标号,并利用优先队列(闲着没事可以手写堆)使得每次推流都是高度最高的顶点,以此减少推流的次数和重标号的次数。
优化:
和ISAP一样的gap优化,如果某个高度不存在,将所有比该高度高的节点标记为不可到达(使它的高度为n+1,这样就会直接向s推流了)。
代码非常恐怖,我没有了打代码的勇气。
时间复杂度$ O(n^2\sqrt m)$,常数较大,导致随机数据下还没有ISAP快。ISAP应该是最牛的。
最小费用最大流
每一条边有了单位流量的花费C[i] 。
看起来好像很毒瘤,就是那种 NOI/NOI+/CTSC 的题目。
(实际上是 提高+/省选- P3381 【模板】最小费用最大流)
但是其实很简单。把bfs替换成最短路算法就可以了。需要使用SPFA。(这里的SPFA与正常SPFA的区别就是:剩余流量为0的边是不走的。所以就不会出现负环了。)
同时,DFS的要求dep[u] + 1 == dep[v]
就改变为了dist[u] + cost[p] == dist[v]
。两者的追求都是走最短路。
当你要修改边的剩余流量的时候,同时计算花费就行了。
注意:一条边的反向边的费用是它的相反数。正因为相反数的存在,所以只能用SPFA而不能用Dij。
注意:因为这是费用流,所以边的代价可能为0,。这样就会出现dfs时在两个节点之间来回跑的现象。这样,就必须要给每一个点打上不会因为dfs的return而false的vis标记,防止去同一个点两次(但是特殊地,去t点又可以去多次)。正是因为这个限制,所以我们就算有了多路增广,一次bfs之后也不能只dfs一次,不然不够。
总结:
- 边的反向边的费用是边的费用的相反数。
- 每次深搜只能搜每个点一次,但是可以搜t点多次。
- 使用SPFA来求最短路(洛谷题解中有一位大佬搞出了Dij的做法,很神仙)
20191111版代码(Dinic)
/* for vjudge
ID: wangyuxi20040901
TASK: ()
LANG: C++
DATE: 20191111 17:24:47
*///using CRLF, UTF-8
#include <bits/stdc++.h>
#define pr printf
#define F(i, j, k) for(register int i = j, kkllkl = k; i <= kkllkl; ++i)
#define G(i, j, k) for(register int i = j, kkllkl = k; i >= kkllkl; --i)
#define clr(a) memset((a), 0, sizeof(a))
#define rg register
using namespace std;
typedef long long ll;
#define isd(x) (('0' <= (x) && (x) <= '9') || (x) == '-')
int rd() {
int ans = 0, sign = 1; char c = getchar();
while(!isd(c)) c = getchar();
if(c == '-') sign = -1, c = getchar();
while(isd(c)) ans = (ans << 3) + (ans << 1) + c - '0', c = getchar();
return sign == 1 ? ans : -ans;
}
#define OJ
// #define DEBUG
/* ------------------------CSYZ1921--------------------------- */
const int N = 5005, M = 50005, INF = 0x3f3f3f3f;
int n, m, s, t;
int tu[N], to[M << 1], nxt[M << 1], cost[M << 1], f[M << 1], tot = 1;
int cur[N];
bool vis[N];
int Maxflow, Mincost; // 最终答案
void cnct(int u, int v, int w, int c) {
to[++tot] = v;
cost[tot] = c;
f[tot] = w;
nxt[tot] = tu[u];
tu[u] = tot;
}
queue<int> Q; bool inque[N]; int dist[N];
bool SPFA() {
F(i, 1, n) dist[i] = 0x3f3f3f3f, inque[i] = false, cur[i] = tu[i];
Q.push(s);
dist[s] = 1;
inque[s] = true;
while(!Q.empty()) {
int u = Q.front(); Q.pop();
inque[u] = false;
for(int v, p = tu[u]; p; p = nxt[p]) {
v = to[p];
if(dist[v] > dist[u] + cost[p] && f[p] > 0) {
dist[v] = dist[u] + cost[p];
if(!inque[v]) Q.push(v), inque[v] = true;
}
}
}
return dist[t] != INF;
}
int dfs(int u, int low) {
int left = low;
vis[u] = true;
if(u == t) {
Maxflow += low;
return low;
}
for(int v, p = cur[u]; p; p = nxt[p]) {
v = to[p];
if((!vis[v] || v == t) && dist[v] == dist[u] + cost[p] && f[p]) {
int gone = dfs(v, min(left, f[p]));
left -= gone;
f[p] -= gone; Mincost += gone * cost[p];
f[p ^ 1] += gone;
if(left <= 0) {
break;
}
}
}
return low - left;
}
void Dinic() {
while(SPFA()) {
vis[t] = true;
while(vis[t]) {
clr(vis);
dfs(s, INF);
}
}
}
int main() {
n = rd(), m = rd(), s = rd(), t = rd();
F(i, 1, m) {
int u = rd(), v = rd(), w = rd(), c = rd();
cnct(u, v, w, c);
cnct(v, u, 0, -c);
}
Dinic();
pr("%d %d\n", Maxflow, Mincost);
return 0;
}
/*
------------------------------------------------------------
g++ -o P3381【模板】最小费用最大流 P3381【模板】最小费用最大流.cpp
./P3381【模板】最小费用最大流
4 5 4 3
4 2 30 2
4 3 20 3
2 3 20 1
2 1 30 9
1 3 40 5
>>
50 280
*/