这几天学了网络流,只想说,真的毒。。。
对于一个网络G = (V , E);对于每一条边(x,y)∈ E;都有一个流量c(x,y)称为边的容量。特别的,若(x,y)∉ E,则
c(x,y)= 0。图中还有两个指定的特殊节点S ∈ V和 T ∈ V
(S ≠ T),分别称为源点和汇点。(以上内容摘自《算法竞赛进阶指南》)
好趴,不说那么高深莫测的话,先上图(s到v2没有流,先别管):
一个网络的一个流就是由一条从s到t的路径和路径上的流量组成的。
打个比方,每一条边就好比一根水管,它的容量就是水管的容量,超过容量水管会爆(砰~),而s是水源,水流源源不断地从s流出,t是。。。要用水的地方(想不出什么好的名词QWQ)。
于是,为了用更多的水,你要让更多的水从s流到t,而最后最多的水流就是该网络的最大流。
还是这个图:
一个大小为16的水流从s出发,到达v1,发现v1到v3容量大小只有12,于是没办法,只能12的水流先过去,剩下的4的水流发现v1还能到达v2,而且容量为10(大于4),可以通过,于是开心地过去啦!!在v3发现只能到达t了,而且容量为20(真大),于是12点的水先去了。。。在v2的水发现也只能去到v4,容量为14,于是通过,在v4同样的,刚刚好通过v4到t,一个从s到t的大小为16的流诞生了!!!
如果一个水管容量为10,其中已经有大小为4的水流过,那么它最多就只能再流过大小为6的水了。。。
如上图,如果s到v2流为0(即未连通),那么该网络的最大流就是刚刚所得到的16;假设s到v2的容量为10,这10的水到v2后去不了v1,因为v1到v3的边已经满流了,而v2到v4的边刚好还剩下10的容量,于是好不容易到达v4,结果发现v4到t的边也满流了,只好去到v3,发现v3还剩8的容量,流8点过去,剩下2点(你自己哭吧)。。。得到的最大流就是16+8=24;
终于讲完了什么是最大流了,思想其实还是很好懂的,可是怎么实现呢??
卖个管(guan)子先。。。
于是,我们的Edmonds-Karp增广路算法诞生了。
介绍一下增广路的概念:若一条从s到t的路径上,各边的剩余容量都大于0,则称这条路径为一条增广路。那么很显然,我们的任务就是不断地寻找增广路,使整个网络的流量增大,直到找不到这样一条增广路为止。
而EK算法的思想就是通过BFS找增广路,直到网络上不存在增广路为止。在每次寻找过程中,我们考虑每一条剩余容量大于0的边,找到一条从s到t路径,然后这条路径是上的最小容量minf就是网络可以增加的流量。
但如果我们第一次流错了怎么办呢?什么意思呢,比如把点比作城市,边比作火车线路,而边的容量就是这条线路剩余的车票数,如果有三个人从城市1来到了城市2,城市2到城市3原本有5张车票,他们用了三张车票去了城市3,现在又有5个人到了城市2,他们也想去城市3,但车票却不够了,不过他们听说城市1有直接到城市3的线路而且车票剩余3张,那么聪明的你肯定想到了,让那三个人回到城市1(就是不来城市2)直接去城市3,这样城市2的这5个人也能去到城市3了,perfect!But,怎么让机器也懂得这样调度呢?
于是,我们可以在存边(x,y)时,同时存一条容量为0的反向边(y,x)(因为还没有人反悔),在BFS的过程中,正向边与反向边同时搜,如果搜到了正向边,表明是直接流过,如果是反向边,表明是反悔了。
也就是说,对于一条增广路,其中的每一条边(x,y)(无论是正向边还是反向边,因为反向边也能反悔)的剩余容量都要减少增广路的流量e,
而它的反向边(y,x)的剩余容量都要增加e(因为最多可以反悔e)。
好了,EK算法的核心已经介绍完了,它的时间复杂度理论上是O($nm^{2}$),但实际运行时远远达不到这个程度,效率较高。
具体细节代码里详解吧。
再贴上代码:
#include<bits/stdc++.h>
using namespace std;
const int M = 100100,inf = 1 << 29;
int n,m,s,t,maxflow,tot = 1;//令tot从1开始累加,方便后面运算,具体看42,43行。
int ver[M*2],edge[M*2],Next[M*2],head[M*2],pre[M*2];
void add(int x,int y,int z){
ver[++tot] = y;edge[tot] = z;Next[tot] = head[x];head[x] = tot;//边权用来存剩余容量。
ver[++tot] = x;edge[tot] = 0;Next[tot] = head[y];head[y] = tot;//反向建边
}
const int N = 10100;
int v[N],incf[N];
bool bfs(){
memset(v,0,sizeof(v));
queue<int>q;
q.push(s);v[s] = 1;
incf[s] = inf;//初始化流量为无穷大,这样才能找到最小值minf。
while(q.size()){
int x = q.front();q.pop();
for(int i = head[x];i;i = Next[i]){
if(edge[i]){//如果当前的边剩余容量大于0,可以选取。
int y = ver[i];
if(v[y]) continue;//选过的点不选。
pre[y] = i;//记录前驱,便于找到路径的实际方案。
incf[y] = min(incf[x],edge[i]);//找路径上的最小流量minf。
q.push(y);v[y] = 1;
if(y == t) return 1;//找到了,就返回1。
}
}
}
return 0;//未找到,说明不存在增广路了,搜索结束,返回0。
}
void update(){
int x = t;
while(x != s){//更新每一条边的剩余容量。
int i = pre[x];
edge[i] -= incf[t];
edge[i^1] += incf[t];//用了成对存储的技巧,从1开始成对存,i^1就可以找到反向边的编号。
x = ver[i^1];//另一个节点,直到倒推到源点s。
}
maxflow += incf[t];//更新最大流。
}
int main(){
cin>>n>>m;
cin>>s>>t;
for(int i = 1;i <= m; i++){
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
add(a,b,c);//加边。
}
while(bfs()) update();
printf("%d",maxflow);
return 0;
}
本蒟蒻的第一篇博客呀,写得不好见谅啊QAQ
886。。。