基础知识我就不再累述了,大家百度百科或找某大牛博客看看就好了
下面是摘自某牛(http://www.cnblogs.com/neverforget/archive/2011/10/20/2210785.html)的一些总结:
第一部分.最大流的算法
下面步入与实际问题更加接近的算法实现部分,首先给出问题,给定一个流网络,求源到汇在单位时间内的最大流量。
最简单而效率较好的算法 是基于增广路的算法,这类算法在王欣上大牛的论文中有详细介绍,但我仍然想谈谈我的想法,希望能起到抛砖引玉的作用。基于增广路的算法主要有两种:MPLA,Dinic,SAP.其中最简单的是MPLA,最实用最简洁也是最多人用的是Dinic,SAP的范围也很广,加上GAP优化后的效率也让人咋舌,这也是最近SAP大泛滥的原因吧!个人比较喜欢Dinic,数据变态就用最高标号预流推进,SAP用的比较少,当然,用什么算法还是看你自己的感觉吧。有些人认为增广路算法格式低效,于是想出了对于每个节点操作的算法,这类算法以预留推进为顶梁柱,MPM也勉强归入这一类吧。
1.MPLA算法
即最短路径增值算法,可以有一个简单的思想,每次都找一条从源到汇的路径来增广,直到不能增广为止,之中算法的正确性是可以保证的,但效率不尽如人意,有些时候,把事情格式化反而有益,这里的MPLA就是这样,它只在层次图中找增广路,构建出层次图之后,用BFS不断增广,直到当前层次图中不再有增广路,再重新构建层次图,如果汇点不在层次图内,则源汇不再连通,最大流已经求出,否则继续执行增广,如此反复,就可以求出最大流,在程序实现时层次图不用被构建出来,只需要BFS出各点的距离标号,找路径时判断对于f(u,v)是否有d[u]+1=d[v]即可。
如果每建一次层次图成为一个阶段,则在最短路径增值算法中,最多有N个阶段,证明略过。
因此在整个算法中,最多有N个阶段,每个阶段构建层次图的BFS时间复杂度为O(m),建N次,因此构建层次图的总时间为O(mn),而在增广过程中,每一次增广至少删除一条边,因此增广m次,加上修改流量的时间,每一阶段的增广时间为O(m*(m+n)),共有N个阶段,所以复杂度为O(n*m*(m+n))=O(nm^2),这也是该算法的时间复杂度。
/************************************************************** Problem: User: youmi Language: C++ Result: Accepted Time: Memory: ****************************************************************/ //#pragma comment(linker, "/STACK:1024000000,1024000000") //#include<bits/stdc++.h> #include <iostream> #include <cstdio> #include <cstring> #include <algorithm> #include <map> #include <stack> #include <set> #include <sstream> #include <cmath> #include <queue> #include <string> #include <vector> #define zeros(a) memset(a,0,sizeof(a)) #define ones(a) memset(a,-1,sizeof(a)) #define sc(a) scanf("%d",&a) #define sc2(a,b) scanf("%d%d",&a,&b) #define sc3(a,b,c) scanf("%d%d%d",&a,&b,&c) #define scs(a) scanf("%s",a) #define sclld(a) scanf("%I64d",&a) #define pt(a) printf("%d ",a) #define ptlld(a) printf("%I64d ",a) #define rep0(i,n) for(int i=0;i<n;i++) #define rep1(i,n) for(int i=1;i<=n;i++) #define rep_1(i,n) for(int i=n;i>=1;i--) #define rep_0(i,n) for(int i=n-1;i>=0;i--) #define Max(a,b) (a)>(b)?(a):(b) #define Min(a,b) (a)<(b)?(a):(b) #define lson (step<<1) #define rson (lson+1) #define esp 1e-6 #define oo 0x7fffffff #define TEST cout<<"*************************"<<endl using namespace std; typedef long long ll; int n,m; const int maxn=200+10; int dis[maxn]; int pc[maxn][maxn]; int bfs() { ones(dis); dis[1]=0; queue<int >q; q.push(1); while(!q.empty()) { int u=q.front(); q.pop(); rep1(v,m) { if(dis[v]==-1&&pc[u][v]) { dis[v]=dis[u]+1; q.push(v); } } } if(dis[m]>0) return 1; return 0; } int dfs(int u,int flow) { int temp; if(u==m) return flow; rep1(v,m) { if(pc[u][v]&&dis[v]==(dis[u]+1)&&(temp=dfs(v,Min(pc[u][v],flow)))) { pc[u][v]-=temp; pc[v][u]+=temp; return temp; } } return 0; } int main() { //freopen("in.txt","r",stdin); while(~sc2(n,m)) { zeros(pc); int u,v; int w; rep1(i,n) { sc2(u,v); sc(w); pc[u][v]+=w; } int flow,ans=0; while(bfs()) { while((flow=dfs(1,oo))!=0) ans+=flow; } pt(ans); } return 0; }
2.Dinic算法
MPLA虽然简单,但经常会点超时,我们把增广过程中的BFS改成DFS,效率会有比较大的提高么?答案是肯定的,至此我们已经得到了Dinic的算法流程,只是将MPLA的增广改为DFS,就能写出那美妙的Dinic了,同样,分析一下时间,在DFS过程中,会有前进和后退两种情况,最多前进后退N次,而增广路最多找M次,再加上N个阶段,所以Dinic的复杂度就是O(mn^2),事实上,它也确实比MPLA快很多,简洁而比较高效,这也是许多OIER选择Dinic的理由了吧,毕竟,写它可能会节省出较长时间来完成其他题目.
/************************************************************** Problem: User: youmi Language: C++ Result: Accepted Time: Memory: ****************************************************************/ //#pragma comment(linker, "/STACK:1024000000,1024000000") //#include<bits/stdc++.h> #include <iostream> #include <cstdio> #include <cstring> #include <algorithm> #include <map> #include <stack> #include <set> #include <sstream> #include <cmath> #include <queue> #include <string> #include <vector> #define zeros(a) memset(a,0,sizeof(a)) #define ones(a) memset(a,-1,sizeof(a)) #define sc(a) scanf("%d",&a) #define sc2(a,b) scanf("%d%d",&a,&b) #define sc3(a,b,c) scanf("%d%d%d",&a,&b,&c) #define scs(a) scanf("%s",a) #define sclld(a) scanf("%I64d",&a) #define pt(a) printf("%d ",a) #define ptlld(a) printf("%I64d ",a) #define rep0(i,n) for(int i=0;i<n;i++) #define rep1(i,n) for(int i=1;i<=n;i++) #define rep_1(i,n) for(int i=n;i>=1;i--) #define rep_0(i,n) for(int i=n-1;i>=0;i--) #define Max(a,b) (a)>(b)?(a):(b) #define Min(a,b) (a)<(b)?(a):(b) #define lson (step<<1) #define rson (lson+1) #define esp 1e-6 #define oo 0x3fffffff #define TEST cout<<"*************************"<<endl using namespace std; typedef long long ll; int n,np,nc,m; const int maxn=200+10; int dis[maxn]; int pc[maxn][maxn]; int bfs() { ones(dis); dis[0]=0; queue<int >q; q.push(0); while(!q.empty()) { int u=q.front(); q.pop(); rep1(v,n) { if(dis[v]==-1&&pc[u][v]) { dis[v]=dis[u]+1; q.push(v); } } } if(dis[n]>0) return 1; return 0; } int dfs(int u,int flow) { int temp; if(u==n) return flow; rep1(v,n) { if(pc[u][v]&&dis[v]==(dis[u]+1)&&(temp=dfs(v,Min(pc[u][v],flow)))) { pc[u][v]-=temp; pc[v][u]+=temp; return temp; } } return 0; } int main() { //freopen("in.txt","r",stdin); while(~scanf("%d%d%d%d",&n,&np,&nc,&m)) { n++; int u,v,w; zeros(pc); rep1(i,m) { scanf(" (%d,%d)%d",&u,&v,&w); u++,v++; //printf("u->%d v->%d ",u,v); pc[u][v]=w; } rep1(i,np) { scanf(" (%d)%d",&v,&w); v++; //printf("v->%d w->%d ",v,w); pc[0][v]=w; } rep1(i,nc) { scanf(" (%d)%d",&u,&w); u++; pc[u][n]=w; } int flow,ans=0; while(bfs()) { while((flow=dfs(0,oo))!=0) ans+=flow; } pt(ans); } return 0; }
3.SAP算法
SAP( shortest augment path )也是找最短路径来增广的算法,有这样一句话:SAP算法更易理解,实现更简单,效率更高,而也有测试表明,SAP加上重要的GAP优化后,效率仅次于最高标号预流推进算法,因此如果你想背一个模板,SAP是最佳选择。SAP在增广时充分的利用了以前的信息,当按照高度找不到增广路时,它会对节点重新标号,h[i]=min{h[j]}+1(c[i,j]>0),这也是SAP比较核心的思想,而根据这个我们可以发现,当高度出现间隙时,一定不会存在增广路了,算法已经可以结束,因此,这里引入间隙优化(GAP),即出现间隙时结束算法。
在算法实现中,初始标号可以全部置为0,在增广过程中在逐渐提升高度,时间上可能会有常数的增加,但不改变渐进时间复杂度。同时为了简洁,SAP实现时用递归,代码不过80行左右。
4.MPM算法
这个算法我还没有实践过,因为它的实现过程比较繁琐,而且时间效率不高,是一个只具有理论价值的算法,这个算法每次都处理单独节点,记每个节点入流和与出流和的最小值作为thoughput(now)(定义在非源汇点),每次先从now向汇推大小为thoughput(now)的流量,在从点now向源点拉大小为thoughput(now)的流量,删除该节点,继续执行直到图中只剩下源汇。时间复杂度为O(n^3),但时间常数较大,时间效率不高。
5.预留推进算法
以上的算法中,基本上都需要从大体上来把握全局,而预留推进算法则是将每一个顶点看作了一个战场,分别对他们进行处理,在处理过程中,存在某些时间不满足流量收支平衡,所以对预先推出的流叫做预流,下面来看算法如何将预流变成最大流的。
预留推进算法有两个主过程,push和relabel,即推进和重标号,它是在模拟水流的过程,一开始先让源的出弧全部饱和,之后随着时间的推移,不断改变顶点的高度,而又规定水流仅能从高处流向低处,所以在模拟过程中,最终会有水流入汇,而之前推出的多余的水则流回了源,那么我们每次处理的是什么节点呢?把当前节点内存有水的节点称为活跃节点,每次对活跃节点执行推流操作,直到该节点不再活跃,如果不能再推流而当前节点仍未活跃节点,就需要对它进行重新标号了,标号后再继续推流,如此重复,直到网络中不再存在活跃节点为止,这时源的流出量就是该网络的最大流。注意,对于活跃节点的定义,不包括源汇,否则你会死的很惨。
朴素的预留推进的效率还过得去,最多进行nm次饱和推进和n^2m次不饱和推进,因此总的时间复杂度为O(mn^2)
事实上,如同增广路算法引入层次图一样,定下一些规则,可以让预留推进算法有更好的时间效率,下面介绍相对而言比较好实现的FIFO预留推进算法,它用一个队列来保存活跃节点,每次从队首取出一个节点进行推进,对一个节点relabel之后把它加到队尾,如此执行,直到队列为空,这样一来,预留推进算法的时间复杂度降为O(n^3),实现的时候,可以加上同样的间隙优化,但注意,出现间隙时不要马上退出,将新标号的的高度置为n+1,继续执行程序,这样会让所有的剩水流回源,满足流量收支平衡,以便最后的统计工作。
下面介绍最后一个,也是编程难度最大,时间表现不同凡响的算法,最高标号预流推进,它的思想是既然水是从高处向低处流的,那么如果从低处开始会做许多重复工作,不如从最高点开始流,留一次就解决问题。再直观一些,引用黑书上的话“让少数的节点聚集大量的盈余,然后通过对这些节点的检查把非饱和推进变成一串连续的饱和推进”。在程序现实现时,用一个表list来储存所有的活跃节点,其中list(h)存储高的为h的活跃节点,同时记录一个level,为最高标号,每次查找时依次从level,level-1……查找,直到找到节点为止,这时从表内删掉这个节点,对它进行Push,Relabel操作,直到该节点不再活跃,继续进行,直到表内不在存在活跃节点。
它的复杂度为O(n^2*m^(1/2)),时间效率很优秀(当然,如果你刻意构造卡预留推进的数据,它比MPLA还慢也是有可能的)。
小结:
网络流的最大流算法种类繁多,时间效率编程复杂度也不尽相同,对于不同的流网络,选择相应的算法,需要在不断实践中摸索,这也是一个菜鸟到大牛的必经之路。在一般题目中,选用Dinic是一个不错的想法,但当我们发现网络特别稠密时,FIFO的预留推进算法就要派上用场了,而时间比较紧但题目数据弱,我们甚至可以采用搜索找增广路的算法。
第三部分 最小费用最大流问题
学习了网络流的最大流算法,一定有一种十分兴奋的感觉,那么,就让你借着这股兴奋劲儿,来学习这一章的最小费用流吧。
最小费用流有两种经典的算法,一种是消圈算法,另一种则是最小费用路增广算法。
第一种,消圈算法。如果在一个流网络中求出了一个最大流,但对于一条增广路上的某两个点之间有负权路,那么这个流一定不是最小费用最大流,因为我们可以让一部分流从这条最小费用路流过以减少费用,所以根据这个思想,可以先求出一个最大初始流,然后不断地通过负圈分流以减少费用,直到流网络中不存在负圈为止。
消圈算法的时间复杂度上限为O(nm^2cw),其中c是最大流量,w为费用最大值,而按特定的顺序消圈的时间复杂度为O(nm^2logn)。这里的时间复杂度分析是按照用bellman-ford算法消圈得到的,用SPFA应该可以得到更优的实际运行时间。
第二种,最小费用路增广算法。这里运用了贪心的思想,每次就直接去找s到t的最小费用路来增广,这样得到的结果一定是最小费用,实现较简单,时间复杂度O(mnv),v为最大流量。用SPFA效果极好,但鉴于SPFA的不确定性,有时为了保险,往往运用重新加权技术,具体实践请通过网络或其他途径获得。
最小费用流的东西并不多,事实上是使用最短路径这种特殊的网络流解决了普遍的网络流问题,只要掌握好基础,程序不难写出。
poj 2195 Going home
/************************************************************** Problem: User: youmi Language: C++ Result: Accepted Time: Memory: ****************************************************************/ //#pragma comment(linker, "/STACK:1024000000,1024000000") //#include<bits/stdc++.h> #include <iostream> #include <cstdio> #include <cstring> #include <algorithm> #include <map> #include <stack> #include <set> #include <sstream> #include <cmath> #include <queue> #include <string> #include <vector> #define zeros(a) memset(a,0,sizeof(a)) #define ones(a) memset(a,-1,sizeof(a)) #define sc(a) scanf("%d",&a) #define sc2(a,b) scanf("%d%d",&a,&b) #define sc3(a,b,c) scanf("%d%d%d",&a,&b,&c) #define scs(a) scanf("%s",a) #define sclld(a) scanf("%I64d",&a) #define pt(a) printf("%d ",a) #define ptlld(a) printf("%I64d ",a) #define rep0(i,n) for(int i=0;i<n;i++) #define rep1(i,n) for(int i=1;i<=n;i++) #define rep_1(i,n) for(int i=n;i>=1;i--) #define rep_0(i,n) for(int i=n-1;i>=0;i--) #define Max(a,b) (a)>(b)?(a):(b) #define Min(a,b) (a)<(b)?(a):(b) #define lson (step<<1) #define rson (lson+1) #define esp 1e-6 #define oo 0x3fffffff #define TEST cout<<"*************************"<<endl using namespace std; typedef long long ll; int n,m; const int maxn=310; char s[maxn][maxn]; struct node { int x,y; node(int l,int r):x(l),y(r){} }; vector<node>man,house; int pp,T; struct side { int u,v,w,res,c,next; }e[maxn*maxn]; int head[maxn<<2]; void init() { T=0; pp=0; man.clear(); house.clear(); ones(head); } void build(int u,int v,int w,int c) { e[T].u=u; e[T].v=v; e[T].w=w; e[T].c=c; e[T].res=T+1; e[T].next=head[u]; head[u]=T++; e[T].u=v; e[T].v=u; e[T].w=0; e[T].c=-c; e[T].res=T-1; e[T].next=head[v]; head[v]=T++; } int inq[maxn<<2]; int dis[maxn<<2]; int pre[maxn<<2]; bool spfa(int st,int ed) { zeros(inq); for(int i=st;i<=ed;i++) dis[i]=oo; dis[st]=0; queue<int >q; q.push(st); while(!q.empty()) { int u=q.front(); q.pop(); inq[u]=0; for(int i=head[u];~i;i=e[i].next) { int v=e[i].v; if(dis[v]>dis[u]+e[i].c&&e[i].w) { dis[v]=dis[u]+e[i].c; pre[v]=i; if(!inq[v]) { inq[v]=1; q.push(v); } } } } if(dis[ed]==oo) return false; return true; } int c,f; void min_cost_max_flow(int st,int ed) { c=0; int p; while(spfa(st,ed)) { f=oo; for(int u=ed;u!=st;u=e[p].u) { p=pre[u]; f=Min(f,e[p].w); } for(int u=ed;u!=st;u=e[p].u) { p=pre[u]; e[p].w-=f; e[e[p].res].w+=f; } c+=dis[ed]*f; } } int main() { //freopen("in.txt","r",stdin); while(~sc2(n,m)&&n+m) { init(); rep1(i,n) scs(s[i]+1); rep1(i,n) rep1(j,m) { if(s[i][j]=='m') { pp++; man.push_back(node(i,j)); } if(s[i][j]=='H') { house.push_back(node(i,j)); } } int st=0,ed=pp<<1|1; rep1(i,pp) { build(st,i,1,0); } rep1(i,pp) { build(pp+i,ed,1,0); } rep0(i,pp) rep0(j,pp) { build(i+1,pp+j+1,1,abs(man[i].x-house[j].x)+abs(man[i].y-house[j].y)); } min_cost_max_flow(st,ed); pt(c); } return 0; }
第四部分 网络流算法的应用
一. 最大流问题。
一般情况下,比较裸的最大流几乎不存在,网络流这种东西考得就是你的构图能力,要不然大家背一背基本算法就都满分了,下面介绍一道比较典型的最大流问题。
问题一:最小路径覆盖问题。
题目链接:http://hzoi.openjudge.cn/never/1004/
最小路径覆盖=|P|-最大匹配数
而最大匹配数可以用匈牙利,也可以用最大流,而两者在这特殊的图中,效率是相同的,而一旦题目有一些变化,网络流可以改改继续用,而匈牙利的局限性较大。
问题二:奶牛航班。
Usaco的赛题,以飞机上的座位作为流量限制,通过实际模型的构建,最终运用最大流算法解决,详解可参考国家集训队论文,具体哪年的忘记了,囧。
最大流实在难已以找到比较有意思的题目,下面进入应用最广泛的最小费用流吧!
二.最小费用流问题(最大收益流问题)
这个问题的模型很多下面就此解析几道例题。
问题一:N方格取数
在一个有m*n 个方格的棋盘中,每个方格中有一个正整数。现要从方格中取数,使任意2 个数所在方格没有公共边,且取出的数的总和最大。
解析:这是一个二分图最大点权独立集问题,就是找出图中一些点,使得这些点之间没有边相连,这些点的权值之和最大。独立集与覆盖集是互补的,求最大点权独立集可以转化为求最小点权覆盖集(最小点权支配集)。最小点权覆盖集问题可以转化为最小割问题解决。
结论:最大点权独立集 = 所有点权 - 最小点权覆盖集 = 所有点权 - 最小割集 = 所有点权 - 网络最大流。
问题还有许多,可以参考网上的网络流与线性规划24题,里面题目比较全面(虽然好多根本用不到网络流)。
最后再提一道题目,说一下最小割的转化建模。
The last问题:黑手党
题目大意:要用最少的人数来切断从A到B的所有路径,每个人只能切断一条边。
分析:显然是一个从A到B的最小割问题,由最大流最小割定理,求A到B 的最大流即可。
结论:网络流问题博大精深,难点在构图,这是一种能力,需要逐渐培养。
总结:关于网络流的介绍到这里也就结束了,但是网络流绝不是仅仅这点东西的,由于个人水平问题,出错或片面的地方还请大牛指正。
网络流题目汇总:
poj 3281 dining 最大流+拆点(把一头牛拆成两头牛,建立边,边权为1)
/************************************************************** Problem: User: youmi Language: C++ Result: Accepted Time: Memory: ****************************************************************/ //#pragma comment(linker, "/STACK:1024000000,1024000000") //#include<bits/stdc++.h> #include <iostream> #include <cstdio> #include <cstring> #include <algorithm> #include <map> #include <stack> #include <set> #include <sstream> #include <cmath> #include <queue> #include <string> #include <vector> #define zeros(a) memset(a,0,sizeof(a)) #define ones(a) memset(a,-1,sizeof(a)) #define sc(a) scanf("%d",&a) #define sc2(a,b) scanf("%d%d",&a,&b) #define sc3(a,b,c) scanf("%d%d%d",&a,&b,&c) #define scs(a) scanf("%s",a) #define sclld(a) scanf("%I64d",&a) #define pt(a) printf("%d ",a) #define ptlld(a) printf("%I64d ",a) #define rep0(i,n) for(int i=0;i<n;i++) #define rep1(i,n) for(int i=1;i<=n;i++) #define rep_1(i,n) for(int i=n;i>=1;i--) #define rep_0(i,n) for(int i=n-1;i>=0;i--) #define Max(a,b) (a)>(b)?(a):(b) #define Min(a,b) (a)<(b)?(a):(b) #define lson (step<<1) #define rson (lson+1) #define esp 1e-6 #define oo 0x3fffffff #define TEST cout<<"*************************"<<endl using namespace std; typedef long long ll; int fd,dk,cw; const int maxn=600+10; struct node { int v,res,w; }; vector<node>vt[maxn]; void build(int u,int v,int w) { vt[u].push_back((node){v,vt[v].size(),w}); vt[v].push_back((node){u,vt[u].size()-1,0}); //printf("vt[%d].rev->%d vt[%d].rev->%d ",u,vt[v].size()-1,v,vt[u].size()-1); } bool vis[maxn]; bool flag[maxn]; int dfs(int u,int t,int flow) { if(u==t) return flow; vis[u]=true; for(int i=0;i<vt[u].size();i++) { node&temp=vt[u][i]; int v=temp.v; if(!vis[v]&&temp.w>0) { int f=dfs(v,t,Min(temp.w,flow)); if(f) { //printf("u->%d v->%d f->%d ",u,v,f); temp.w-=f; vt[v][temp.res].w+=f; return f; } } } return 0; } int max_flow() { int ans=0,flow; int temp=fd+dk+cw+cw+1; while(1) { zeros(vis); flow=dfs(0,temp,1); if(flow) ans+=flow; else return ans; } } int main() { //freopen("in.txt","r",stdin); while(~sc3(cw,fd,dk)) { zeros(vt); rep1(i,cw) { int tot1,tot2,t1,t2; sc2(tot1,tot2); rep1(j,tot1) { sc(t1); build(dk+cw+i,dk+cw+cw+t1,1); } rep1(j,tot2) { sc(t2); build(t2,dk+i,1); } } int temp=fd+dk+cw+cw+1; rep1(i,dk) build(0,i,1); rep1(i,fd) build(dk+cw+cw+i,temp,1); rep1(i,cw) build(dk+i,dk+cw+i,1); pt(max_flow()); } return 0; }
(更新未完)