最大流
基本概念
无论是电网、水管网、还是其他地一些网络,他们都有一个共同点:网络传输都有方向 和 容量。设有带权图 $G=(V,E) , V={s,v_1,v_2,v_3,\cdots,t}$ 。 在图G中有两个特殊的点s和t,s称为源点,t称为汇点。图中各边地方向表示允许的流向,边上地权值表示该边允许通过的最大可能流量cap,且$cap\geq 0$ 称为边地容量。而且如果边集合E含有一条边$(u,v)$,必然不存在反向的平行边$(v,u)$,称这样的有向带权图为网络。 因为如果存在可以直接合并掉。
现在有一家公司想要从工厂s运往仓库t,找到一家代理公司。代理公司安排了若干货车和运输线路,中间要经过若干城市,边上的数值代表两个城市之间每天最多运送产品的数量。
这就类似一个地下水管网络,我们看不到水在地下管道内是怎么流动的,但是直到从进水口流进多少水,就会从出水口流出来多少水。
网络流
网络流即网络上的流,是定义在网络边集E上的一个非负函数$flow={flow(u,v)}$, flow(u,v)是边上的流量
可行流:
满足以下两个性质的网络流flow称为可行流
我们要从s将货物运送到t,需要满足几个条件
- 容量约束,每个管道的实际流量 flow 不能超过该管道的最大容量 cap
- 流量守恒,除了源点是全部汇出没有汇入、汇点是全部汇入没有汇出的,其他中间节点流入和流出的量都是一模一样的,不允许中间节点有存货
源点s:
源点主要是流出,但也有可能流入,例如货物运出后检测出的一些不合格产品需要返厂,这对源点来说就是流入量。因此,源点的净流出值f=流出量之和-流入量之和。即: $f=\sum{(s,x)\in E}flow(s,x)-\sum{(y,s)\in E}flow(y,s)$
从下图也可以反映出流量守恒和容量约束
现在我们提出网络最大流问题:在满足容量约束和流量守恒的前提下,在流网络中找到一个净输出最大的网络流。
我们先来学习 Ford&Fullkerson提出的方法。该方法的基本思想是在残余网络中找可增广路,然后在实流网络中沿可增广路增流,直到不存在可增广路为止。这里要注意,找可增广路的方法有很多。
残余网络:
每个网络G及其上的一个流flow,都对应一个残余网络G*.G* 和 G结点集相同,而网络G中的每条边对应G*中的一条边或者两条边。在残余网络中,与网络边对应的同向边是可增量(即还可以增加多少流量),反向边是实际流量
那么我们可以根据原来的图来画出残余网络
可增广路:
是残余网络G* 中一条从源点s到汇点t的简单路径。例如:$s-v_1-v_3-t$ 就是一条可增广路。如下图
然后我们可以沿着增广路径来增加我们的货物数量。但是注意这里增加的量是路径上数值最小的路段。比如说这里能增加的量是5。在增加了货物数量之后,我们还需要修改增广路上的值。比如说这$s-v_1$从9改成4,$v_1-v_3$的线路被抹去,$v_3-t$的线路从12改为7.
可增广量
可增广量是指在可增广路p上每条边可以增加的流量最小值。求网络G的最大流,首先在残余网络中找可增广路,然后再实流网络G‘中沿可增广路增流,直到不存在可增广路为止。这是实流网络G’就是最大流网络。
可增广路增流
增流操作可以分为两个过程:一是在实流网络中增流,二是在残余网络中减流。因为残余网络中可增广路上的边值表示可增量,在实流网络中流量增加了,那么可增量就少了。
直接拿给我们一个残余网络,我们是不能判断哪一条是可增量哪一条是实际流量的
实流网络增流
在实流网络中沿着可增广路增流: 可增广路上同向边增加流量d,反向边减少流量d。
残余网络减流
在残余网络中沿着可增广路减流:可增广路上的同向边减少流量d,反向边增加流量d
增广路算法
增广路定理 :设flow是网络G的一个可行流,如果不存在从源点s到汇点t关于flow的可增广路p,则flow是G的一个最大流
增广路算法: 增广路算法的基本思想是在残余网络中找到可增广路,然后在实流网络中沿着可增广路增流,在残余网络中沿可增广路减流;继续在残余网络中找可增广路,知道不存在可增广路为止。此时实流网络中的可行流就是所求的最大流。
最短增广路算法
如何找到一条可增广路呢?我们可以设置最大容量优先,也可以是最短路径(广度优先)优先。 Edmonds-Karp算法就是以广度优先的增广路算法,又称为最短增广路算法(Shortest Augument Path, SAP)
步骤
- 初始化可行流 flow为零流,$vis[]$数组为$false$,$pre[]$数组为-1,vis数组代表该节点是否已经访问过,pre数组代表该节点的前驱结点
- 令$vis[s] = true$,并将s加入队列q
- 如果队列不空,就继续下一步,否则算法结束,并返回最大流
- 队头元素new出队,在残余网络中检查new的所有邻接结点i。 如果i未被访问,那么就访问i结点并令 $vis[i]=true,pre[i] = new$。 如果 $i=t$ ,说明已经到达汇点,找到一条可增广路,转向第(5) 步;否则结点i加入队列q,转向第3步。
- 从汇点开始,通过前驱数组$pre[]$ ,逆向找可增广路上每条边值的最小值,即可增量d。
- 在实流网络中增流,在残余网络中减流,$Maxflow+=d$ ,并转向第(2)步
比如说这个图,我们可以先来手工计算改图的最大流。
时间复杂度
从算法的描述中可以看出,找到一条可增广路的时间是 $O(E)$,最多会执行 $O(VE)$次,因为关键边的总数为$O(VE)$.因此总的时间复杂度为 $O(VE^2)$ ,其中 V为结点个数,E为边的数量
空间复杂度:
使用了一个二维数组表示实流网络,因此空间复杂度为 $O(V^2)$
代码剖析
1 |
|
但是在建图的时候,假设cap=10,flow=3,但这只是正向边。我们我们还需要录入反向边,也就是将cap设置为0,flow设置为-3这种网络叫做混合网络,将残余网络和实流网络混合在一起了:flow为正数的就是实流网络,负数为残余网络。
这样也能达到残余网络的效果,因为在增广路径上查找的时候是根据cap>flow走的,走反向边可以重新调整流量。
下面是在链式前向星中插入边的函数,这里一开始需要将flow设置为零流
1 | void add(int u,int v,int c){ |
下面是广搜的算法
首先每次广搜之前都需要对前驱数组和标志数组进行一个重置
然后是对链式前向星进行广搜。
在这里我们pop掉队列里的头节点后,我们遍历它的周围节点,符合条件的就将它加入到队列当中。条件是头结点到该结点的这条边的 cap必须大于flow,而且是未被访问过的结点。同时我们维护一个最小值d,始终记录着这条增广路的可增广量
如果v等于t,就说明找到了一条从s到t的增广路径,那么我们就可以进入下一步骤,即增流减流
1 | bool bfs(int s, int t){ |
接下来是EK方法,我们用while循环将所有的增广路径找出。然后通过第二个while循环对增广路径进行输出,并对实流网络增流和残余网络减流。这里有一个小技巧,对于链式前向星的对应边,只需要 $E[i\wedge1]$ 即可,因为我们录入的时候都是成对成对的。一直到输出s为止。
1 | int EK(int s,int t){ |
输出网络函数,这里我们输出了链式前向星的结构
1 | void printg(int n){ |
还有一个输出实流边的函数。
1 | void printflow(){//输出实流边 |