[洛谷P3376题解]网络流(最大流)的实现算法讲解与代码
定义
对于给定的一个网络,有向图中每个的边权表示可以通过的最大流量。假设出发点S水流无限大,求水流到终点T后的最大流量。
起点我们一般称为源点,终点一般称为汇点
内容前置
1.增广路
在一个网络从源点S
到汇点T
的一条各边剩余流量都大于0(还能让水流通过,没有堵住)的一条路。
2.分层
预处理出源点到每个点的距离(每次寻找增广路都要,因为以前原本能走的路可能因为水灌满了,导致不能走了).作用是保证只往更远的地方放水,避免兜圈子或者是没事就走回头路(正所谓人往高处走水往低处流).
3.当前弧优化
每次增广一条路后可以看做“榨干”了这条路,既然榨干了就没有再增广的可能了。但如果每次都扫描这些“枯萎的”边是很浪费时间的。那我们就记录一下“榨取”到那条边了,然后下一次直接从这条边开始增广,就可以节省大量的时间。这就是当前弧优化
具体怎么实现呢,先把链式前向星的head数组复制一份,存进cur数组,然后在cur数组中每次记录“榨取”到哪条边了。
[#3 引用自](Dinic当前弧优化 模板及教程 - Floatiy - 博客园 (cnblogs.com))
解决算法
Ford-Fulkerson 算法(以下简称FF算法)
FF算法的核心是找增广路,直到找不到为止。(就是一个搜索,用尽可能多的水流填充每一个点,直到没有水用来填充,或者没有多余的节点让水流出去)。
但是这样的方法有点基于贪心的算法,找到反例是显而易见的,不一定可以得到正解。
为了解决这种问题,我们需要一个可以吃后悔药的方法——加反向边。
原本我们的DFS是一条路走到黑的,现在我们每次进入一个节点,把水流送进去,同时建立一个权值与我们送入的水流量相等,但是方向相反的路(挖一条路让水流能够反向流回来,相当于给水流吃一颗后悔药)。
我们给了FF算法一颗后悔药之后就可以让他能够找到正确的最大流。
Ford-Fulkerson算法的复杂度为 O ( e × f ) O(e \times f) O(e×f) ,其中 e e e 为边数, f f f为最大流
上代码。
#include <iostream>
#include <cstring>
using namespace std;
#define INF 0x3f3f3f3f3f3f3f3f
typedef long long ll;
// Base
const int N= 256;
const int M= 8192*2;
// End
// Graph
int head[N],nxt[M],to[M];
ll dis[M];
int p;
inline void add_edge(int f,int t,ll d)
{
to[p]=t;
dis[p]=d;
nxt[p]=head[f];
head[f]=p++;
}
// End
int n,m,s,t;
// Ford-Fulkerson
bool vis[N];
ll dfs(int u,ll flow)//u是当前节点 , flow是送过来的水量
{
if(u==t)// End,水送到终点了
return flow;
vis[u]=true;
for(int i=head[u];i!=-1;i=nxt[i])
{
ll c;//存 送水下一个节点能通到终点的最大流量
if(dis[i]>0 //如果水流还能继续流下去
&& !vis[to[i]] //并且要去的点没有其他的水流去过
&& (c=dfs(to[i],min(flow,dis[i])))!=-1//根据木桶效应,能传给下一个节点的水量取决于当前节点有的水量与管道(路径)能够输送的水量的最小值
//要保证这条路是通的我们才可以向他送水,不然就是浪费了
) {
dis[i]-=c;//这个管道已经被占用一部分用来送水了,需要减掉
dis[i^1]+=c;//给他的反向边加上相同的水量,送后悔药
//至于为什么是这样取出反向边,下面有讲
return c;
}
}
return -1;
}
// End
int main()
{
ios::sync_with_stdio(true);
memset(head,-1,sizeof(head));// init
cin>>n>>m>>s>>t;
for(int i=1;i<=m;i++)
{
int u,v,w;cin>>u>>v>>w;
add_edge(u,v,w);
add_edge(v,u,0);//建立一条暂时无法通水的反向边(后面正向边送水后,需要加上相同的水量)
//第一条边 编号是 0 ,其反向边为 1, 众所周知的 奇数^1=奇数-1, 偶数^1=偶数+1 ,利用这种性质 ,我们就可以很快求出反向边,或者反向边得出正向边(这里说的正反只是相对)
}
//Ford-Fulkerson
ll ans = 0;
ll c;
// 假设我们的水无限多
while((c=dfs(s,INF)) != -1) //把源点还能送出去的水全部都送出去,直到送不到终点
{
memset(vis,0,sizeof(vis)); //重新开始送没送出去的水
ans+=c;//记录总的水量
}
cout<<ans<<endl;
return 0;
}
可以看出效率比较低,我这里开了O2也过不了模板题。
Edmond-Karp 算法(以下简称EK算法)
上面FF算法太慢了,原因是因为FF算法太死脑筋了,非要等现在节点水灌满了,才会灌其他的(明明有一个更大的水管不灌)。另外他有时候还非常谦让,等到明明走到了,却要返回去等别人水灌好,再灌自己的。
其实,EK算法便是FF算法的BFS版本。复杂度为 O ( v × e 2 ) O(v \times e^2) O(v