几个月前学过,然而一下就忘记了,于是决定系统的复习一下。
关于网络流各路神犇早已有很好的讲解,于是我就整理一下(其实我是蒟蒻,看到的果断关掉吧)
http://blog.youkuaiyun.com/leolin_/article/details/7202691
残余网络: 两个点之间有一个流的限制,那么假如有一个流流过,那么残余网络记录的是在进行几次操作之后,两点间还可以通过多少。
增广路径:(引用他人的话)假如有这么一条路,这条路从源点开始一直一段一段的连到了汇点,并且,这条路上的每一段都满足流量<容量,注意,是严格的<,而不是<=。那么,我们一定能找到这条路上的每一段的(容量-流量)的值当中的最小值delta。我们把这条路上每一段的流量都加上这个delta,一定可以保证这个流依然是可行流。这样我们就得到了一个更大的流,他的流量是之前的流量+delta,而这条路就叫做增广路。
最大流最小割定理:一个流是最大流,当且仅当它的残留网络不包含增广路径。
割:http://blog.youkuaiyun.com/kk303/article/details/6728400
c[][]残余网络
反向边:( 最难理解的是加反向边)比如从u到v流过x,那么c[u][v]-=x,c[v][u]+=x讲的精简点,反向边的作用是给反向边的作用就是给程序一个可以后悔的机会 ,细细品味吧
1、Ford-Fulkerson算法
不断寻找增光路。
Ford-Fulkerson算法在实际中并不常用,但是它提供了一种思想:先找到一条从源点到汇点的增广路径,这条路径的容量是其中容量最小的边的容量。然后,通过不断找增广路,一步步扩大流量,当找不到增广路时,就得到最大流了(最大流最小割定理)。
寻找通路的时候可以用DFS,BFS最短路等算法。就这两者来说,BFS要比DFS快得多,但是编码量也会相应上一个数量级。
http://codevs.cn/problem/1993/
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <queue>
#include <cstring>
using namespace std;
int n,m,start,end,zgpath[210],map[210][210],flow[210];
queue<int>q;
int bfs(void){
while(!q.empty())q.pop();
memset(zgpath,-1,sizeof(zgpath));
zgpath[start]=0,flow[start]=99999999;
q.push(start);
while(!q.empty()){
int x=q.front();
q.pop();
if(x==end)break;
for(int i=1;i<=m;i++){
if(i!=start&&zgpath[i]==-1&&map[x][i]){
flow[i]=flow[x]<map[x][i] ? flow[x] : map[x][i];
q.push(i);
zgpath[i]=x;
}
}
}
if(zgpath[end]==-1)return -1;
return flow[end];
}
int zuidaliu(void){
int ans_max=0,ans_;
while((ans_=bfs())!=-1){
ans_max+=ans_;
int now=end;
while(now!=start){
int pre=zgpath[now];
map[pre][now]-=ans_;
map[now][pre]+=ans_;
now=pre;
}
}
return ans_max;
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
int a,b,c;
cin>>a>>b>>c;
map[a][b]+=c;
}
start=1,end=m;
cout<<zuidaliu();
return 0;
}
2、压入重标记push_relabel算法O(VE)
Push-relabel用到一个很有趣的概念一Preflow(前置流)他允许流进的量比流出的量还要多,有水来就先流进来,流不出去再说。
Push-relabel算法的流程如下:
1 )我们先假定一个高度函数 h ( u ) ,他代表u点的高度,只有
h ( u )比较高的点才能够将水流到h ( u )比较低的点;
2 ) 在程序一开始的时候,让source node的高度是n ( node数) , 其它点的高度都是 0 ,这样source node才有足够的高度可以流往其它地方 ;
3 ) 然后, 让source node往其它所有跟他直接相邻的node ,都流水管宽度的水量 ( 流过去之后当然要计算剩下的网络情形,即计算 residual edges(残留网络)
4 )对所有active node( 目前有水的 node )做 relabel(重标记)的动作: 在当某个node明明有水,但是他所连出去的所有对象的 h ( u ) 都比他还高, 则让他的h ( u ) 增加为至少有一条水管可以流出去的量,也就是让这个有水的 active node的高度变成比他连往的”高度最小的 n o d e ”+l , ( 流过去之后还是要计算剩下的网络情形
5 )对所有可以做push(压入)动作的node做push的动作。 所谓的Push动作是指 : 当某个node有水,并且他有可以流出去的边, 且他刚好比可以流出去的那个点高度高一点点 ( 高度恰好比他高 1 ) ,那就把某个node 的水流过去,要流多少呢?以下两者取 min。 流出去的水管的量( 也就是说,这个active node的水量很多, 足够把这条水管塞满( 饱和) ,这个时候就叫做saturating Push)(饱和压入)某个 n o d e 现在的水量( 这个 n o d e的水量不足以把流出去的这个水管填满,称作non saturating Push(不饱和压入)
6 )重复Relabel和Push的工作,一直到没有active node为止,此时从source node所流出的总流量( P r e f l o w) ,就是这个图的最大流量。
Push-relabel algorithm 提供了最大流另一方向的思考,且就效率而言,Push -Relabel的复杂度为 o (vE )
这里以 POJ 1459为例
- #define MIN INT_MIN
- #define MAX INT_MAX
- #define N 110
- int min(int a,int b){return a>b?b:a;}
- int c[N][N];//残留容量
- int ef[N];//顶点余流
- int h[N];//顶点高度
- int n;
- int push_relabel(int s,int t){
- int i,j;
- int ans = 0;
- memset(h,0,sizeof(h));
- h[s] = t+1;//源点初始高度
- memset(ef,0,sizeof(ef));
- ef[s] = MAX;//源点初始余流
- queue<int> qq;
- qq.push(s);
- while(!qq.empty()){
- int u = qq.front();
- qq.pop();
- for(i=0;i<=t;i++){
- int p;
- int v = i;
- if(c[u][v]<ef[u])p = c[u][v];
- else p = ef[u];
- if(p>0 && (u==s || h[u] == h[v] +1)){
- c[u][v] -= p;
- c[v][u] += p;
- if(v==t)ans+=p;//如果到达了汇点,就将流值加入到最大流中
- ef[u] -= p;
- ef[v] += p;
- if(v!=s && v!=t)qq.push(v);//只有既不是源点也不是汇点才进队
- }
- }
- //如果不是源点且仍有余流,则重标记高度再进队。
- //这里只是简单的将高度增加了一个单位,也可以像上面所说的一样赋值为最低的相邻顶点的高度高一个单位
- if(u!= s && u!=t && ef[u]>0) {
- h[u]++;
- qq.push(u);
- }
- }
- return ans;
- }
- int main(){
- int np,nc,m;
- while(scanf("%d%d%d%d",&n,&np,&nc,&m) != -1){
- int s = n,t = n+1;
- int i,j;
- memset(c,0,sizeof(c));
- char ss[30];
- for(i=0;i<m;i++){
- int u,v,w;
- scanf("%s",ss);
- sscanf(ss,"(%d,%d)%d",&u,&v,&w);
- c[u][v] += w;
- }
- for(i=0;i<np;i++){
- int u,w;
- scanf("%s",ss);
- sscanf(ss,"(%d)%d",&u,&w);
- c[s][u] += w;
- }
- for(i=0;i<nc;i++){
- int v,w;
- scanf("%s",ss);
- sscanf(ss,"(%d)%d",&v,&w);
- c[v][t] += w;
- }
- printf("%d\n",push_relabel(s,t));
- }
- return 0;
- }
EK算法基于Ford-Fulkerson算法,唯一的区别是将第 4 行用BFS(广度优先搜索)来实现对增广路径 p 的计算。EK算法伪代码基本和上边的Ford-Fulkerson算法一样。类似用DFS实现的还有Dinic算法。它们都属于SAP(Shortest Augmenting Path)算法,从英文即可看出,它们每次都在寻找最短增广路。对于EK算法,每次用一遍 BFS 寻找从源点 s 到终点 t 的最短路作为增广路径,然后增广流量 f 并修改残量网络,直到不存在新的增广路径。E-K 算法的时间复杂度为 O(VE^2),适用于稀疏边,由于 BFS 要搜索全部小于最短距离的分支路径之后才能找到终点,因此频繁的 BFS 效率是比较低的。实践中此算法使用的机会较少。
这里以 POJ 1273 为例,这里可以作为EK模板
- #define MIN INT_MIN
- #define MAX INT_MAX
- #define N 204
- int c[N][N];//边容量
- int f[N][N];//边实际流量
- int pre[N];//记录增广路径
- int res[N];//残余网络
- queue<int> qq;
- void init(){
- while(!qq.empty())qq.pop();
- memset(c,0,sizeof(c));
- memset(f,0,sizeof(f));
- }
- int EK(int s,int t){
- int i,j;
- int ans=0;
- while(1){
- memset(res,0,sizeof(res));
- res[s] = MAX;//源点的残留网络要置为无限大!否则下面找增广路出错
- pre[s] = -1;
- qq.push(s);
- //bfs找增广路径
- while(!qq.empty()){
- int x = qq.front();
- qq.pop();
- for(i=1;i<=t;i++){
- if(!res[i] && f[x][i] < c[x][i]){
- qq.push(i);
- pre[i] = x;
- res[i] = min(c[x][i] - f[x][i], res[x]);//这里类似dp,如果有增广路,那么res[t]就是增广路的最小权
- }
- }
- }
- if(res[t]==0)break;//找不到增广路就退出
- int k = t;
- while(pre[k]!=-1){
- f[pre[k]][k] += res[t];//正向边加上新的流量
- f[k][pre[k]] -= res[t];//反向边要减去新的流量,反向边的作用是给程序一个后悔的机会
- k = pre[k];
- }
- ans += res[t];
- }
- return ans;
- }
- int main(){
- int n,m;
- while(scanf("%d%d",&n,&m) != -1){
- int i,j;
- init();
- while(n--){
- int a,b,v;
- scanf("%d%d%d",&a,&b,&v);
- c[a][b]+=v;
- }
- printf("%d\n",EK(1,m));
- }
- return 0;
- }
四、Improved SAP(ISAP)算法
ISAP字面意思是改良的最短增广路算法。关于ISAP,一位叫 DD_engi 的神牛讲非常清楚,引用一下:
SAP算法(by dd_engi):求最大流有一种经典的算法,就是每次找增广路时用BFS找,保证找到的增广路是弧数最少的,也就是所谓的 Edmonds-Karp 算法。可以证明的是在使用最短路增广时增广过程不超过 V * E次,每次 BFS 的时间都是O(E),所以 Edmonds-Karp 的时间复杂度就是O(V * E^2)。
如果能让每次寻找增广路时的时间复杂度降下来,那么就能提高算法效率了,使用距离标号的最短增广路算法就是这样的。所谓距离标号,就是某个点到汇点的最少的弧的数量(另外一种距离标号是从源点到该点的最少的弧的数量,本质上没什么区别)。设点 i 的标号为D[i],那么如果将满足D[i] = D[j] + 1的弧(i,j))叫做允许弧,且增广时只走允许弧,那么就可以达到“怎么走都是最短路”的效果。每个点的初始标号可以在一开始用一次从汇点沿所有反向边的BFS求出,实践中可以初始设全部点的距离标号为0,问题就是如何在增广过程中维护这个距离标号。
维护距离标号的方法是这样的:当找增广路过程中发现某点出发没有允许弧时,将这个点的距离标号设为由它出发的所有弧的终点的距离标号的最小值加一。这种维护距离标号的方法的正确性我就不证了。由于距离标号的存在,由于“怎么走都是最短路”,所以就可以采用DFS找增广路,用一个栈保存当前路径的弧即可。当某个点的距离标号被改变时,栈中指向它的那条弧肯定已经不是允许弧了,所以就让它出栈,并继续用栈顶的弧的端点增广。为了使每次找增广路的时间变成均摊O(V),还有一个重要的优化是对于每个点保存“当前弧”:初始时当前弧是邻接表的第一条弧;在邻接表中查找时从当前弧开始查找,找到了一条允许弧,就把这条弧设为当前弧;改变距离标号时,把当前弧重新设为邻接表的第一条弧,还有一种在常数上有所优化的写法是改变距离标号时把当前弧设为那条提供了最小标号的弧。当前弧的写法之所以正确就在于任何时候我们都能保证在邻接表中当前弧的前面肯定不存在允许弧。
还有一个常数优化是在每次找到路径并增广完毕之后不要将路径中所有的顶点退栈,而是只将瓶颈边以及之后的边退栈,这是借鉴了Dinic算法的思想。注意任何时候待增广的“当前点”都应该是栈顶的点的终点。这的确只是一个常数优化,由于当前边结构的存在,我们肯定可以在O(n)的时间内复原路径中瓶颈边之前的所有边。
优化:
1.邻接表优化:
如果顶点多的话,往往N^2存不下,这时候就要存边:
存每条边的出发点,终止点和价值,然后排序一下,再记录每个出发点的位置。以后要调用从出发点出发的边时候,只需要从记录的位置开始找即可(其实可以用链表)。优点是时间加快空间节省,缺点是编程复杂度将变大,所以在题目允许的情况下,建议使用邻接矩阵。
2.GAP优化:
如果一次重标号时,出现距离断层,则可以证明ST无可行流,此时则可以直接退出算法。
3.当前弧优化:
为了使每次找增广路的时间变成均摊O(V),还有一个重要的优化是对于每个点保存“当前弧”:初始时当前弧是邻接表的第一条弧;在邻接表中查找时从当前弧开始查找,找到了一条允许弧,就把这条弧设为当前弧;改变距离标号时,把当前弧重新设为邻接表的第一条弧。
另外,ISAP简化的描述是:程序开始时用一个反向 BFS 初始化所有顶点的距离标号,之后从源点开始,进行如下三种操作:(1)当前顶点 i 为终点时增广 (2) 当前顶点有满足 dist[i] = dist[j] + 1 的出弧时前进 (3) 当前顶点无满足条件的出弧时重标号并回退一步。整个循环当源点 s 的距离标号 dist[s] >= n 时结束。对 i 点的重标号操作可概括为 dist[i] = 1 + min{dist[j] : (i,j)属于残量网络Gf}。
借用庄神的模板 http://www.zlinkin.com/?p=34 ,用它来过POJ 3469简直无敌!比Dinic快好几倍
- const int MAXN=20010;
- const int MAXM=500010;
- int n,m;//n为点数 m为边数
- int h[MAXN];
- int gap[MAXN];
- int p[MAXN],ecnt;
- int source,sink;
- struct edge{
- int v;//边的下一点
- int next;//下一条边的编号
- int val;//边权值
- }e[MAXM];
- inline void init(){memset(p,-1,sizeof(p));eid=0;}
- //有向
- inline void insert1(int from,int to,int val){
- e[ecnt].v=to;
- e[ecnt].val=val;
- e[ecnt].next=p[from];
- p[from]=eid++;
- swap(from,to);
- e[ecnt].v=to;
- e[ecnt].val=0;
- e[ecnt].next=p[from];
- p[from]=eid++;
- }
- //无向
- inline void insert2(int from,int to,int val){
- e[ecnt].v=to;
- e[ecnt].val=val;
- e[ecnt].next=p[from];
- p[from]=eid++;
- swap(from,to);
- e[ecnt].v=to;
- e[ecnt].val=val;
- e[ecnt].next=p[from];
- p[from]=eid++;
- }
- inline int dfs(int pos,int cost){
- if (pos==sink){
- return cost;
- }
- int j,minh=n-1,lv=cost,d;
- for (j=p[pos];j!=-1;j=e[j].next){
- int v=e[j].v,val=e[j].val;
- if(val>0){
- if (h[v]+1==h[pos]){
- if (lv<e[j].val) d=lv;
- else d=e[j].val;
- d=dfs(v,d);
- e[j].val-=d;
- e[j^1].val+=d;
- lv-=d;
- if (h[source]>=n) return cost-lv;
- if (lv==0) break;
- }
- if (h[v]<minh) minh=h[v];
- }
- }
- if (lv==cost){
- --gap[h[pos]];
- if (gap[h[pos]]==0) h[source]=n;
- h[pos]=minh+1;
- ++gap[h[pos]];
- }
- return cost-lv;
- }
- int sap(int st,int ed){
- source=st;
- sink=ed;
- int ans=0;
- memset(gap,0,sizeof(gap));
- memset(h,0,sizeof(h));
- gap[st]=n;
- while (h[st]<n){
- ans+=dfs(st,INT_MAX);
- }
- return ans;
- }
对于EK算法与ISAP算法的区别:
EK算法每次都要重新寻找增广路,寻找过程只受残余网络的影响,如果改变残余网络,则增广路的寻找也会随之改变;SAP算法预处理出了增广路的寻找大致路径,若中途改变残余网络,则此算法将重新进行。EK处理在运算过程中需要不断加边的最大流比SAP更有优势。
*****************************************************************************************************************************************************
本文只介绍基础的网络流知识,网络流非常强大,许多问题都可以转化为网络流模型,进一步的,可以去看看Starfall大神的这篇《【网络流】总结》,这篇只是一个目录性质,里边很多超链接,后面资源更丰富。其中提到了6篇国家集训队论文,正好电脑里都有,就打包传到优快云了,点击跳转到下载页面。我只看过其中的两三篇,这帮高中生写的实在牛叉。
记得当年看时,觉得这篇《最大流在信息学竞赛中应用的一个模型》当做入门非常好,看完后会发现,原来组合数学都可以用最大流来解,还有什么不可以的。另外,最经典,最全面的要数胡波涛这篇的《最小割模型在信息学竞赛中的应用》,如果想深入学习网络流,这帮家伙的论文绝对不能错过。
以上转自http://mindlee.net/2011/11/19/network-flow/ 有删改