【算法】网络流之最大流~~

流啊流~流啊流~,流成最大流~
网络流是个是个神奇的玩意~
今天先写一个最大流的博文记录咯。
=======================散发着神秘气息的分割线=======================
FCBayern!
首先要学习网络流,我们要知道一些定义哦。当然这些定义和图的定义是差不多的呢~
V表示整个图中的所有结点的集合.
E表示整个图中所有边的集合.
G = (V,E) ,表示整个图.
s表示网络的源点,t表示网络的汇点.
对于每条边(u,v),有一个容量c(u,v) (c(u,v)>=0)
如果c(u,v)=0,则表示(u,v)不存在在网络中。
如果原网络中不存在边(u,v),则令c(u,v)=0
对于每条边(u,v),有一个流量f(u,v)。
网络流,顾名思意就是要“流”,那么用一个简单的说法就是像水管一样,有水在里面流~,比如这幅图:左边表示里面的水流量,右边表示这个水管的最大流量。
【算法】网络流之最大流~~
文章图片

那么网络流的限制是神马捏?
、容量限制: f[u,v]<=c[u,v]
2、反对称性:f[u,v] = - f[v,u]
3、流量平衡: 对于不是源点也不是汇点的任意结点,流入该结点的流量和等于流出该结点的流量和。
结合反对称性,流量平衡也可以写成:
∑u∈Vf(u,v)
只要满足这三个性质,就是一个合法的网络流。
那最大流就是指能流到汇点的最大流量。
定义一个网络的流量(记为|f|)
∑v∈Vf(s,v)
求一个|f|合法的最大值。
当然这里还要引入一个叫残量网络的定义,在最大流算法中会得到运用:
为了更方便算法的实现,一般根据原网络定义一个残量网络。其中r(u,v)为残量网络的容量。
r(u,v)=c(u,v)–f(u,v)
通俗地讲:就是对于某一条边(也称弧),还能再有多少流量经过。
Gf 残量网络, Ef 表示残量网络的边集。
【算法】网络流之最大流~~
文章图片

【算法】网络流之最大流~~
文章图片

在这里引用两张ppt来讲述,不好意思忘了是谁的 0.0
除了残量路径,还有一个东西叫后向弧:
其中像(v1,s)这样的边称为后向弧,它表示从v1到s还可以增加4单位的流量。
那么问题来了:
从v1到s不是和原网络中的弧的方向相反吗?显然“从v1到s还可以增加4单位流量”这条信息毫无意义。那么,有必要建立这些后向弧吗?
答案是肯定的!
显然,例1中的画出来的不是一个最大流。
但是,如果我们把 s?>v2?>v1?>t 这条路径经过的弧的流量都增加2,就得到了该网络的最大流。
注意到这条路径经过了一条后向弧:(v2,v1)。
如果不设立后向弧,算法就不能发现这条路径。
从本质上说,后向弧为算法纠正自己所犯的错误提供了可能性,它允许算法取消先前的错误的行为(让2单位的流从v1流到v2)。
这样子,我们经过很多次处理后,就可以得到最大流哦~不过事实上后向弧这玩意和普通路径没什么区别,不过是为了处理更简单罢了。
为了实现算法,我们还要知道一个叫做增广路的东西:
增广路定义:在残量网络中的一条从s通往t的路径,其中任意一条弧(u,v),都有r[u,v]>0。
求最大流的方法Ford-Fulkerson
Ford-Fulkerson方法依赖于三种重要思想,这三个思想就是残留网络,增广路径和割。Ford-Fulkerson方法是一种迭代的方法。开始时,对所有的u,v∈V有f(u,v)=0,即初始状态时流的值为0。在每次迭代中,可通过寻找一条“增广路径”来增加流值。增广路径可以看成是从源点s到汇点t之间的一条路径,沿该路径可以压入更多的流,从而增加流的值。反复进行这一过程,直至增广路径都被找出来,根据最大流最小割定理,当不包含增广路径时,f是G中的一个最大流。在算法导论中给出的Ford-Fulkerson实现代码如下

FORD_FULKERSON(G,s,t) for each edge(u,v)∈E[G] do f[u,v] <— 0 f[v,u] <— 0 while there exists a path p from s to t in the residual network Gf do cf(p) <— min{ cf(u,v) : (u,v) is in p } for each edge(u,v) in p do f[u,v] <— f[u,v]+cf(p)//对于在增广路径上的正向的边,加上增加的流 f[v,u] <— -f[u,v]//对于反向的边,根据反对称性求

第1~3行初始化各条边的流为0,第4~8就是不断在残留网络Gf中寻找增广路径,并沿着增广路径的方向更新流的值,直到找不到增广路径为止。而最后的最大流也就是每次增加的流值cf(p)之和。在实际的实现过程中,我们可以对上述代码做些调整来达到更好的效果。如果我们采用上面的方法,我们就要保存两个数组,一个是每条边的容量数组c,一个就是上面的每条边的流值数组f,在增广路径中判断顶点u到v是否相同时我们必须判断c[u][v]-f[u][v]是否大于0,但是因为在寻找增广路径时是对残留网络进行查找,所以我们可以只保存一个数组c来表示残留网络的每条边的容量就可以了,这样我们在2~3行的初始化时,初始化每条边的残留网络的容量为G的每条边的容量(因为每条边的初始流值为0)。而更新时,改变7~8行的操作,对于在残留网络上的边(u,v)执行 c[u][v]?=cf(p) ,而对其反向的边(v,u)执行 c[v][u]+=cf(p) 即可。
现在剩下的最关键问题就是如何寻找增广路径。而Ford-Fulkerson方法的运行时间也取决于如何确定第4行中的增广路径。如果选择的方法不好,就有可能每次增加的流非常少,而算法运行时间非常长,甚至无法终止。对增广路径的寻找方法的不同形成了求最大流的不同算法,这也是Ford-Fulkerson被称之为“方法”而不是“算法”的原因。
要实现这个方法,就遇到了三个问题:
(1)最多要增广多少次?
可以证明 最多O(VE)次增广 可以达到最大流 证明在这最后吧!
(2)如何找到一条增广路?
先明确什么是增广路: 增广路是这样一条从s到t的路径 路径上每条边残留容量都为正。把残留容量为正的边设为可行的边 那么我们就可以用简单的BFS得到边数最少的增广路。
(3)BFS得到增广路之后 这条增广路能够增广的流值, 是路径上最小残留容量边决定的。
把这个最小残留容量MinCap值加到最大流值Flow上, 同时路径上每条边的残留容量值减去MinCap。
最后,路径上每条边的反向边残留容量值要加上MinCap
接下来介绍两中找增广路的方法:
第一种是Edmonds-Karp(EK),这种做法就是用BFS来找到一条最短的增广路径,仅此而已。
模板:
#include #include #include #include #include using namespace std; const int MAX=0x7fffffff; const int MAXN=202; int flow[MAXN][MAXN],cap[MAXN][MAXN]; int vis[MAXN],pre[MAXN]; int n,m,u,v,w,ans; queueq; bool bfs(int s,int t) { memset(vis,0,sizeof(vis)); vis[s]=MAX; q.push(s); while(!q.empty()) { int now=q.front(); q.pop(); for(int i=1; i<=m; ++i) { if(flow[now][i]

其中,cap表示可流流量,而flow表示当前流量,vis[t]表示最小残留容量。
这种方法显然易懂,就不多说了,和普通bfs是差不多了。
第二种是Dinic算法。
这种算法号称是时间复杂度最优的算法,它的主旨在于把多次找增广路的过程简化,构造成类似一座楼,一层一层的层次图。
建立一个辅助网络L,L与原网络G具有相同的节点数,但边上的容量有所不同,在L上进行增广,将增广后的流值回写到原网络上,再建立当前网络的辅助网络,如此反复,达到最大流。分层的目的是降低寻找增广路的代价。
算法步骤如下:
STEP1:建造原网络G的一个分层网络L。STEP2:用增广路算法计算L的最大流F,若在L中找不到增广路,算法结束。SETP3:根据F更新G中的流f,转STEP1。

分层网络的构造算法:
STEP1:标号源节点s,M[s]=0。STEP2:调用广度优先遍历算法,执行一步遍历操作,当前遍历的弧e=v1v2,令r=G.u(e)-G.f(e)。 若r>0,则(1)若M[v2]还没有遍历,则M[v2]=M[v1]+1,且将弧e加入到L中,容量L.u(e)=r。(2)若M[v2]已经遍历且M[v2]=M[v1]+1,则将边e加入到L中,容量L.u(e)=r。(3)否则L.u(e)=0。否则L.u(e)=0。

重复本步直至G遍历完。其中的G.u(e)、G.f(e)、L.u(e)分别表示图G中弧e的容量上界和当前流量,图L中弧e的容量上界。
算法的时间复杂度为O(2mn)。其中m为弧的数目,是多项式算法。邻接表表示图,空间复杂度为O(n+m)。
模板有两个哦~
第一个是自己码的,可能更清晰。
#include #include #include #include using namespace std; const int INF=0x7fffffff; int N,M; int level[205]; int Si,Ei,Ci; struct Dinic { int c; int f; }edge[205][205]; bool dinic_bfs()//构造层次网络 { queueq; memset(level,0,sizeof(level)); //初始化顶点的层次 为0 q.push(1); level[1]=1; int u,v; while(!q.empty()) { u=q.front(); q.pop(); for(v=1; v<=M; v++) { if(!level[v]&&edge[u][v].c>edge[u][v].f)//即顶点未被访问过,顶点u,v,存在边 { level[v]=level[u]+1; //给顶点标记层次 q.push(v); } } } return level[M]!=0; //若返回false表明 汇点不在层次网络中 } int dinic_dfs(int u,int cp)//进行增广 { int tmp=cp; int v,t; if(u==M) return cp; for(v=1; v<=M&&tmp; v++) { if(level[u]+1==level[v]) { if(edge[u][v].c>edge[u][v].f) { t=dinic_dfs(v,min(tmp,edge[u][v].c-edge[u][v].f)); edge[u][v].f+=t; edge[v][u].f-=t; tmp-=t; } } } return cp-tmp; } int dinic()//求出最大流 { int sum,tf; sum=tf=0; while(dinic_bfs()) { while(tf=dinic_dfs(1,INF)) { sum+=tf; } } return sum; } int main() { while(~scanf("%d%d",&N,&M)) { memset(edge,0,sizeof(edge)); while(N--) { scanf("%d%d%d",&Si,&Ei,&Ci); edge[Si][Ei].c+=Ci; //防止重边 } int ans=dinic(); printf("%d\n",ans); } return 0; }

第二个是wjw提供的真的模板:(可以这很模板)
int level[NMax]; int mkLevel() { for(int i = (level[0] = 0) + 1; i < N; i++)level[i] = -1; static int Q[NMax], bot; Q[(bot = 1) - 1] = 0; for(int top = 0; top < bot; top++) { int x = Q[top]; for(edge *p = E[x]; p; p = p->next)if(level[p->e] == -1 && p->f) level[Q[bot++] = p->e] = level[x] + 1; } return level[N - 1] != -1; } int extend(int a, int b) { int r = 0, t; if(a == N - 1)return b; for(edge *p = E[a]; p && r < b; p = p->next) if(p->f && level[p->e] == level[a] + 1) { t = p->f; if(t > b - r)t = b - r; t = extend(p->e, t); r += t; p->f -= t; OPT(p)->f += t; } if(!r)level[a] = -1; return r; } int Dinic() { int ret = 0, t; while(mkLevel())while((t = extend(0, 1000000000)))ret += t; return ret; }

我要补一个用邻接表写的:
const int INF=0x7fffffff; int head[50000],dis[3000],f,n,m; int IN[3000],OUT[3000]; int t,a,b,h,g,sum,ans; struct node { int to,next,w; }; node edge[50000]; void add(int u,int v,int w) { edge[f].to=v; edge[f].next=head[u]; edge[f].w=w; head[u]=f++; edge[f].to=u; edge[f].next=head[v]; edge[f].w=0; head[v]=f++; } int bfs() { int i,x,v; memset(dis,0,sizeof(dis)); dis[0]=1; queueq; q.push(0); while(!q.empty()) { x=q.front(); q.pop(); for(i=head[x]; i!=-1; i=edge[i].next) { v=edge[i].to; if(edge[i].w&&dis[v]==0) { dis[v]=dis[x]+1; if(v==n+1)return 1; q.push(v); } } } return 0; }int dfs(int s,int cur_flow) { int i,v,tmp,dt=cur_flow; if(s==n+1)return cur_flow; for(i=head[s]; i!=-1; i=edge[i].next) { v=edge[i].to; if(edge[i].w&&dis[s]==dis[v]-1) { int flow=dfs(v,min(dt,edge[i].w)); edge[i].w-=flow; edge[i^1].w+=flow; dt-=flow; } } return cur_flow-dt; }int dinic() { int tt=0; while(bfs()) tt+=dfs(0,INF); return tt; }

其实最后还有一种算法叫ISAP,据说是SAP算法的IMPROVE版本,于是乎,我不准备讲了,好麻烦,直接引用吧:
网络流——ISAP详解
====================以下为引用内容====================
ISAP 算法就是不停地找最短增广路,找到之后增广;如果遇到死路就 retreat,直到发现s, t不连通,算法结束。找最短路本质上就是无权最短路径问题,因此采用 BFS 的思想。具体来说,使用一个数组d,记录每个节点到汇点t的最短距离。搜索的时候,只沿着满足d[u]=d[v]+1的边u→v(这样的边称为允许弧)走。显然,这样走出来的一定是最短路。
原图存在两种子图,一个是残量网络,一个是允许弧组成的图。残量网络保证可增广,允许弧保证最短路(时间界较优)。所以,在寻找增广路的过程中,一直是在残量网络中沿着允许弧寻找。因此,允许弧应该是属于残量网络的,而非原图的。换句话说,我们沿着允许弧,走的是残量网络(而非原图)中的最短路径。当我们找到沿着残量网络找到一条增广路,增广后,残量网络肯定会变化(至少少了一条边),因此决定允许弧的d数组要进行相应的更新(顺便提一句,Dinic 的做法就是每次增广都重新计算d数组)。然而,ISAP 「改进」的地方之一就是,其实没有必要马上更新d数组。这是因为,去掉一条边只可能令路径变得更长,而如果增广之前的残量网络存在另一条最短路,并且在增广后的残量网络中仍存在,那么这条路径毫无疑问是最短的。所以,ISAP 的做法是继续增广,直到遇到死路,才执行 retreat 操作。
说到这里,大家应该都猜到了,retreat 操作的主要任务就是更新d数组。那么怎么更新呢?非常简单:假设是从节点u找遍了邻接边也没找到允许弧的;再设一变量m,令m等于残量网络中u的所有邻接点的d数组的最小值,然后令d[u]等于m+1即可。这是因为,进入 retreat 环节说明残量网络中u和 t已经不能通过(已过时)的允许弧相连,那么u和t实际上在残量网络中的最短路的长是多少呢?(这正是d的定义!)显然是残量网络中u的所有邻接点和t的距离加1的最小情况。特殊情况是,残量网络中u根本没有邻接点。如果是这样,只需要把d[u]设为一个比较大的数即可,这会导致任何点到u的边被排除到残量网络以外。(严格来说只要大于等于∣V∣即可。由于最短路一定是无环的,因此任意路径长最大是∣V∣?1)。修改之后,只需要把正在研究的节点u沿着刚才走的路退一步,然后继续搜索即可。
讲到这里,ISAP 算法的框架内容就讲完了。对于代码本身,还有几个优化和实现的技巧需要说明。
算法执行之前需要用 BFS 初始化d数组,方法是从t到s逆向进行。算法主体需要维护一个「当前节点」u
,执行这个节点的前进、retreat 等操作。记录路径的方法非常简单,声明一个数组p,令p[i]等于增广路上到达节点i的边的序号(这样就可以找到从哪个顶点到的顶点i)。需要路径的时候反向追踪一下就可以了。
判断残量网络中s,t不连通的条件,就是d[s]≥∣V∣ 。这是因为当s,t不连通时,最终残量网络中s将没有任何邻接点,对s的 retreat 将导致上面条件的成立。
GAP 优化。
GAP 优化可以提前结束程序,很多时候提速非常明显(高达 100 倍以上)。GAP 优化是说,进入 retreat 环节后,u,t
之间的连通性消失,但如果u是最后一个和t距离d[u](更新前)的点,说明此时s,t也不连通了。这是因为,虽然u,t已经不连通,但毕竟我们走的是最短路,其他点此时到t的距离一定大于d[u](更新前),因此其他点要到t,必然要经过一个和t距离为d[u](更新前)的点。GAP 优化的实现非常简单,用一个数组记录并在适当的时候判断、跳出循环就可以了。
另一个优化,就是用一个数组保存一个点已经尝试过了哪个邻接边。寻找增广的过程实际上类似于一个 BFS 过程,因此之前处理过的邻接边是不需要重新处理的(残量网络中的边只会越来越少)。具体实现方法直接看代码就可以,非常容易理解。需要注意的一点是,下次应该从上次处理到的邻接边继续处理,而非从上次处理到的邻接边的下一条开始。
最后说一下增广过程。增广过程非常简单,寻找增广路成功(当前节点处理到t
)后,沿着你记录的路径走一遍,记录一路上的最小残量,然后从s到t更新流量即可。
====================以上为引用内容====================
#include #include #include #include using namespace std; #define MAXN 10005 #define MAXE 200000 #define INF 1e9 int tmp,src,des,cnt; int n,m; struct Edge{ int from, to; int next,cap; }edge[MAXE]; int head[MAXN]; int gap[MAXN],dep[MAXN],cur[MAXN], stack[MAXN], top; int ISAP() { int cur_fLow,max_fLow,u,insert,i; memset(dep,0,sizeof(dep)); memset(gap,0,sizeof(gap)); memcpy(cur, head, n); max_fLow = 0; u = src; top = 0; while(dep[src] < n) { if(u == des) { cur_fLow = INF; for(i = 0; i < top; ++i) { if(cur_fLow > edge[stack[i]].cap) { insert = i; cur_fLow = edge[stack[i]].cap; } } for(i = 0; i < top; ++i) { edge[stack[i]].cap -= cur_fLow; edge[stack[i]^1].cap += cur_fLow; } max_fLow += cur_fLow; u = edge[ stack[top = insert]].from; } for(i = cur[u]; i != -1; i = edge[i].next) if((edge[i].cap > 0) && (dep[u] == dep[edge[i].to]+1)) break; if(i != -1) { stack[top++] = i; u = edge[i].to; } else { if(0 == --gap[dep[u]]) break; int minn = n; for(i = head[u]; i != -1; i = edge[i].next) { if(edge[i].cap > 0) minn = (minn > dep[edge[i].to]) ? (cur[u]= i, dep[edge[i].to]) : minn; } ++gap[dep[u] = minn + 1]; if(u != src) u = edge[stack[--top]].from; } } return max_fLow; } void addedge(int u,int v,int f) { edge[cnt].next = head[u]; edge[cnt].from = u; edge[cnt].to = v; edge[cnt].cap = f; head[u] = cnt++; edge[cnt].next = head[v]; edge[cnt].from = v; edge[cnt].to = u; edge[cnt].cap = 0; head[v] = cnt++; } int main() { while(scanf("%d%d",&m,&n)!=EOF) { cnt=0; src = https://www.it610.com/article/0; des = n-1; memset(head,-1,sizeof(head)); while(m--) { int a, b, c; scanf("%d%d%d", &a, &b, &c); --a, --b; addedge(a, b, c); //addedge(b, a, c); 如果是无向边的加上这句 } int ans=ISAP(); printf("%d\n",ans); } return 0; }

这就是传说中的ISAP,据说效率比Dinic还高诶!
还有个更变态的预流推进:
预流推进算法给每一个顶点一个标号h(v),表示该点到t的最短路(在残量网络中)。
第一步hights()过程,就是BFS出初始最短路,计算出每一个结点的h(v)。//可以看作是汇点有“吸力”,使每个结点有不同的负压,在“负压”作用下把来自源点的流吸过去。
预流推进算法的特征是运用了预流来加快运算。预流说明图中的结点(除s, t),仅需要满足流入量 >= 流出量。其中流入量>流出量的结点,我们称之为活动结点。/换句话说就是有流可吸,还没吸到地方。/我们的算法就是不断地将活动结点,变为非活动结点,使得预流成为可行流。
算法过程prepare(),即首先将与s相连的边设为满流,并将这时产生的活动结点加入队列Q。这是算法的开始。
以后便重复以下过程直到Q为空:
(1).选出Q的一个活动结点u。并依次判断残量网络G’中每条边(u, v),若h(u) = h(v) + 1,则顺着这些边推流,直到Q变成非活动结点(不存在多余流量)。(Push推流过程)//同时把所有v加入活动结点的队列。
(2).如果u还是活动结点。则需要对u进行重新标号:h(u) = min{h(v) + 1},其中边(u,v)存在于G’ 中。然后再将u加入队列。(reCalc过程)//后面都满流时就吸不动了,负压自然也要重新计算。
可以证明,通过以上算法得到的结果就是最大流。
//显然每次循环后标号和残量网络都是相容的。算法结束时Q为空,只可能是没有活动结点。因为一开始就把从源所有的流推了出来,只可能是要么能够推到汇要么最后退回源。显然,一开始源的标号最高,退回源说明源汇之间已被切断,否则总能杀出一条增广路来。
如果该算法的Q是标准的FIFO队列,则时间复杂度为(n2m),/最高标号不会超过n(超过时必无到汇的路径),所以n个点每个最多重新标号n次,两次标号之间m条边每条最多推流一次。/如果是优先队列,并且标号最高的点优先的话,我们就得到了最高标号预流推进算法,其时间复杂度仅为(n2sqrt(m)),/复杂度分析进行中……/算是比较快的最大流算法了。
模板题转自这里
#include #include#define DEBUG#ifdef DEBUG #define debug(...) printf( __VA_ARGS__) #else #define debug(...) #endif#define N 102 #define MAX_INT 2000000#define min(a, b) ((a) < (b) ? (a) : (b))intgraph[N][N]; inth[N]; inte[N]; intn; int push_relabel(int s, int t) { intmax_flow, u, v, d, done, relabel, min_height; memset(h, 0, sizeof(h)); memset(e, 0, sizeof(e)); h[s] = n; for (u = 1; u <= t; u++) { if (graph[s][u] > 0) { e[u] = graph[s][u]; e[s] -= graph[s][u]; graph[u][s] = graph[s][u]; graph[s][u] = 0; } }for (; ; ) { done = 1; for (u = s+1; u < t; u++) { if (e[u] > 0) { done = 0; //先假设顶点u需要relabel relabel = 1; for (v = s; v <= t && e[u] > 0; v++) {/* 检查能push的顶点 */ if (graph[u][v] > 0 && h[u] > h[v]) { //push relabel = 0; d = min(graph[u][v], e[u]); e[u] -= d; e[v] += d; graph[u][v] -= d; graph[v][u] += d; debug("push %d --%d--> %d, e[%d] = %d\n", u, d, u, u, e[u]); } } //没有可以push的顶点,执行relabel if (relabel) { //relabel min_height = MAX_INT; for (v = s; v <= t; v++) { if (graph[u][v] > 0 && h[v] < min_height) { min_height = h[v]; } } h[u] = 1 + min_height; debug("relabel %d height to %d\n", u, h[u]); } } } if (done) { /* 除源点和汇点外,每个顶点的e[i]都为0 */ max_flow = 0; for (u = s; u <= t; u++) { if (graph[t][u] > 0) { max_flow += graph[t][u]; } } break; } } return max_flow; }int main() { intnp, nc, m, u, v, w; while (scanf("%d", &n) != EOF) { n += 2; /* 添加源点和汇点 */ scanf("%d %d %d", &np, &nc, &m); memset(graph, 0, sizeof(graph)); //输入m条边 while (m--) { scanf(" (%d,%d)%d", &u, &v, &w); graph[u+1][v+1] = w; } //输入np个power station while (np--) { scanf(" (%d)%d", &u, &w); graph[0][u+1] = w; } //输入nc个consumer while (nc--) { scanf(" (%d)%d", &u, &w); graph[u+1][n-1] = w; } printf("%d\n", push_relabel(0, n-1)); } return 0; }

【【算法】网络流之最大流~~】哈利路亚~真tm!!!!!神坑~~!!!!

    推荐阅读