网络流最大流
这应该算是网络流里面最简单的了
但我依旧是一脸懵逼(毕竟老师在讲的时候因为看不见白板就自己在自闭地看着博客)
但我还是找到了可读性极强的博客,并且懂得了皮毛
可读性极强的博客:https://www.cnblogs.com/SYCstudio/p/7260613.html#4331173
通常情况
首先对于最大流的一般例题通常都是类似于:有一座城有无限的水,另一座想通过一些管道获得尽可能多的水
概念
首先(我发现我好喜欢首先啊)要理解几个概念
1.源点(无限水城):只有流出去的边
2.汇点(不水城(没有信仰,缺少划水精神的城)):只有流进来的边
3.容量(管道允许的最大流量)
4.流量(目前管道流过的水量)
5.残量:容量 - 流量
几个基本性质
基本性质一:
对于任何一条流,总有流量<=容量
这是很显然的
基本性质二
对于任何一个不是源点或汇点的点u,总有
∑p∈Ek[p][u]==∑q∈Ek[u][q](其中k[i][j]表示i到j的流量)∑p∈Ek[p][u]==∑q∈Ek[u][q](其中k[i][j]表示i到j的流量)
这个也很显然,即一个点(除源点和汇点)的入流和出流相等
基本性质三
对于任何一条有向边(u,v),总有
k[u][v]==−k[v][u]k[u][v]==−k[v][u]
这个看起来并不是很好理解,它的意思就是一条边的反边上的流是这条边的流的相反数,可以这么想,就是如果有k[u][v]的流从u流向v,也就相当于有-k[v][u]的流从v流向u。这条性质非常重要。
网络流最大流
网络流的最大流算法就是指的一个流量的方案使得网络中流量最大。
网络流最大流的求解
网络流的所有算法都是基于一种增广路的思想,下面首先简要的说一下增广路思想,其基本步骤如下:
1.找到一条从源点到汇点的路径,使得路径上任意一条边的残量>0(注意是小于而不是小于等于,这意味着这条边还可以分配流量),这条路径便称为增广路
2.找到这条路径上最小的F[u][v](我们设F[u][v]表示u->v这条边上的残量即剩余流量),下面记为flow
3.将这条路径上的每一条有向边u->v的残量减去flow,同时对于起反向边v->u的残量加上flow(为什么呢?我们下面再讲)
4.重复上述过程,直到找不出增广路,此时我们就找到了最大流
这个算法是基于增广路定理(Augmenting Path Theorem): 网络达到最大流当且仅当残留网络中没有增广路(由于笔者知识水平不高,暂且不会证明)
举个例子:
为什么要连反向边
我们知道,当我们在寻找增广路的时候,在前面找出的不一定是最优解,如果我们在减去残量网络中正向边的同时将相对应的反向边加上对应的值,我们就相当于可以反悔从这条边流过。
比如说我们现在选择从u流向v一些流量,但是我们后面发现,如果有另外的流量从p流向v,而原来u流过来的流量可以从u->q流走,这样就可以增加总流量,其效果就相当于p->v->u->q,用图表示就是:
图中的蓝色边就是我们首次增广时选择的流量方案,而实际上如果是橘色边的话情况会更优,那么我们可以在v->u之间连一条边容量为u->v减去的容量,那我们在增广p->v->u->q的时候就相当于走了v->u这条"边",而u->v的流量就与v->u的流量相抵消,就成了中间那幅图的样子了。
如果是v->u时的流量不能完全抵消u->v的,那就说明u还可以流一部分流量到v,再从v流出,这样也是允许的。
一个小技巧
虽然说我们已经想明白了为什么要加反向边,但反向边如何具体实现呢?笔者在学习网络流的时候在这里困扰了好久,现在简要的总结在这里。
首先讲一下邻接矩阵的做法,对于G[u][v],如果我们要对其反向边进行处理,直接修改G[v][u]即可。
但有时会出现u->v和v->u同时本来就有边的情况,一种方法是加入一个新点p,使u->v,而v->u变成v->p,p->u。
另一种方法就是使用邻接表,我们把边从0开始编号,每加入一条原图中的边u->v时,加入边v->u流量设为0,那么这时对于编号为i的边u->v,我们就可以知道i^1就是其反向边v->u。
朴素算法的低效之处
虽然说我们已经知道了增广路的实现,但是单纯地这样选择可能会陷入不好的境地,比如说这个经典的例子:
我们一眼可以看出最大流是999(s->v->t)+999(s->u->t),但如果程序采取了不恰当的增广策略:s->v->u->t
我们发现中间会加一条u->v的边
而下一次增广时:
若选择了s->u->v->t
然后就变成
这是个非常低效的过程,并且当图中的999变成更大的数时,这个劣势还会更加明显。
怎么办呢?
这时我们引入Dinic算法
Dinic算法
主体
int dinic() { int ans=0; while(bfs()) ans+=dfs(s,100000007);//源头水量无限,赋极大值 return ans; } //当然也可以这样写 int dinic() { int ans=0; while(bfs()) while(int d=dfs(s,10000007)) ans+=d; return ans; }
BFS部分
bool bfs() { queue < int > Q; memset(deep,0,sizeof(deep)); while(!Q.empty()) Q.pop();//预处理,清空队列,deep Q.push(s); deep[s]=1;//加入源头 do{ int u=Q.front(); Q.pop(); for(int i=last[u];i!=-1;i=nxt[i]) if(wi[i]>0 && deep[to[i]]==0)//边有残量 点未被占领 { deep[to[i]]=deep[u]+1; Q.push(to[i]); } }while(!Q.empty()); if(deep[t]==0) return 0;//未经过t点 返回false return 1; }
DFS部分
int dfs(int u,int dist) { if(u==t) return dist;//到达终点 for(int i=last[u];i!=-1;i=nxt[i]) if(deep[to[i]]==deep[u]+1 && wi[i]!=0)//分层正确 与 有剩余残量 { int di=dfs(to[i],min(dist,wi[i])); if(di>0) { wi[i]-=di;//正向边减 wi[i^1]+=di;//反向边加 return di; } } return 0; }
就这样结束了 (主要是我当前优先弧没学)
!!!