网络流问题:最大流及其算法
一、概念引入
流网络:
G=(V,E)是一个有向图,其中每条边(u,v)∈E均有一个非负容量c(u,v)>=0。如果(u,v)不属于E,则假定c(u,v)=0。流网络中有两个特别的顶点:源点s和汇点t。下图展示了一个流网络的实例(其中斜线左边的数字表示实际边上的流f(u,v),右边的数字表示边的最大容量c(u,v),1为源点,7为汇点):
图a
残余网络:
对于已经找到一条从S 到T的路径的网络中,该边当前流量为f(u,v),只要在这条路径上,把f(u,v)的值更新为C(u,v)-f(u,v),并且添加反向弧C(v,u),即可得到残余网络。
例如有以下流网络
图b
其残余网络如下:
图c
- 在残余网络中有以下几个概念:
-
饱和弧:即f(u,v)=c(u,v),当前流量为最大流量,图b中4-7;
-
非饱和弧:即f(u,v)<c(u,v),当前流量小于最大流量,图b中2-3;
-
零流弧:即f(u,v)=0,当前流量为零,图b中3-5;
-
非零流弧:即f(u,v)>0,当前流量不为零.
-
增广路径:
流网络的增广路径Path为残留网络上从S到T的一条简单路径P,P满足以下条件:(向前的意义为沿着源点指向汇点的方向)- P中所有前向弧都是非饱和弧,
- P中所有后向弧都是非零弧.
图b中1,2,4,7就是一条增广路径,还有1,3,4,7。
-
- 最大流问题:
给定一个流网络,给出每条边能流过的最大流量,求源点到汇点的最大流量,称其为最大流(MaxFlow)。
下面给出一个通俗点的解释
比如你家是汇点,自来水厂是源点,然后自来水厂和你家之间修了很多条水管子接在一起,水管子规格不一,有的容量大,有的容量小。
然后问自来水厂开闸放水,你家收到水的最大流量是多少,如果自来水厂停水了(也就是让每个管道里流过的水量都为0),你家那的流量就是0, 当然不是最大的流量;但是你给自来水厂交了很多钱,自来水厂拼命水管里通水,让每根管道都达到最大流量(容量),这时就达到了最大流。
二.算法描述
- Ford-Fulkerson 方法
FF(Ford-Fulkerson)方法的主要思想就是任意寻找增广路径,找出增广路径上每一段[容量-流量]的最小值delta,然后调整网络,直到找不出增广路径为止。最总所有找出的delta的和即为最大流。
之所以FF称为方法,是因为它一般只作为描述算法的思想,实现时任意寻找增广路径,而有些路径并不能到达汇点,使得搜索效率降低,所以实际一般采用EK(Edmonds-Karp)算法来进行优化。
Edmonds_Karp算法
EK(Edmonds-Karp)算法的核心思想和EK一样,也是不断寻找增广路径,求最小delta,直到最后找不到增广路径时停止。但实现时采用了BFS(广度优先搜索)的方法来对EK算法进行优化,降低时间复杂度。对于一个有V个顶点,E条边的流网络来说,时间复杂度为O(VE^2)。
例题:现有一个供水网络,从水库到目标居民区有若干个中继站,这三者间又有若干型号不同的管道相连,为了简单起见,水库用1表示,中继站用2-7表示,目标居民区用8表示,整个网络有15条水管,各个管道的规格与题意相关的是其容量,一定不能超过该容量,否则会发生爆管的危险事故。问:水库怎样供水能够让目标居民区得到最大的水量?注:中继站不会消耗水,各管道有方向。
思路:(N为节点数,m为源点,t为汇点)
-
采用EK算法求最大流。
-
用邻接矩阵保存流网络,记录各个管道的容量capacity[N+1][N+1](数组为N+1长度是因为节点序号从1开始,代码中N直接采用的可能的最大值)。
-
再创建一个各个管道上当前流量的邻接矩阵flow[N+1][N+1],然后从以各个管道中流量为0作为初始条件进行BFS搜索增广路径。
-
其中需要一个向前节点的数组pre[N+1]来记录每一轮寻找的增广路径,并且因为初始化为-1,最后填入的节点号[1,N]可作为访问的标志,并且pre[t]为-1时还能作为没有增广路径的标识来退出循环。
-
每一轮搜索出一条增广路径,在搜索期间,不断对比前后路径中当前管的容量中流量和前一段管中流量,取最小值,则可在每一轮得出flow[t]为这一轮找出的增广路径的最小容量delta,加到最终结果sumflow中,然后进行调整capacity邻接矩阵。
-
调整的方法:根据pre数组从汇点往回找,不停的将每一段的正向减去delta,负向加上delta形成反向弧。直到源点。
-
EK算法的精华是:负向加上delta,这一步能避免在做增广路时可能会阻塞后面的增广路,因为做增广路本来是有个顺序才能找完最大流的(举例),但我们是任意找的,为了修正,就每次将流量加在了反向弧上,让后面的流能够进行自我调整,剩余网络的更新。
具体举例:
- 人为去找:
- 1->3->7->8, delta =9
- 1->2->6->8, delta = 5
- 1->4->5->8, delta = 10
- 1->3->6->8, delta = 1
- 1->4->5->2->6->8, delta = 3
- 但程序并不是这样走的
- 1->2->6->8, delta= 5
- 1->3->6->8, delta = 5
- 1->3->7->8, delta = 5
- 1->4->5->8, delta = 10
- 1->4->5->2->6->3->7->8, delta = 3
可以发现6->3为逆向,因为有了反向弧调整了网络,所以才让其行的通,可以将反向弧理解为当前管道“退回”一部分水,改走另一条路。
代码
#include <iostream>
#include <queue>
#include <string.h>
using namespace std;
#define arraysize 201
int maxData = 0x7fffffff;
int capacity[arraysize][arraysize]; //记录残留网络的容量
int flow[arraysize]; //标记从源点到当前节点实际还剩多少流量可用
int pre[arraysize]; //标记在这条路径上当前节点的前驱,同时标记该节点是否在队列中
int n,m;
queue<int> myqueue;
/*
Sample
Input
15 8
1 3 10
1 2 5
1 4 15
3 2 4
2 4 4
5 2 6
3 7 9
3 6 15
2 6 8
4 5 16
7 6 15
6 5 15
7 8 10
6 8 10
5 8 10
Output
28
*/
int BFS(int src,int des)//每一次BFS 找一条增广路径
{
int i,j;
while(!myqueue.empty()) //队列清空
myqueue.pop();
for(i=1;i<m+1;++i)
{
pre[i]=-1;
}
pre[src]=0;
flow[src]= maxData;
myqueue.push(src);
while(!myqueue.empty())
{
int front_index = myqueue.front();
myqueue.pop();
if(front_index == des) //找到了增广路径
break;
for(i=1;i<m+1;++i)
{
//if(i!=src)printf("test:capacity[%d][%d]=%d and pre[%d]=%d ",front_index,i,capacity[front_index][i],i,pre[i]) ;
if(i!=src && capacity[front_index][i]>0 && pre[i]==-1)
{
pre[i] = front_index; //记录前驱
flow[i] = min(capacity[pre[i]][i],flow[front_index]); //关键:迭代的找到增量
//printf("pre=%d and flow[%d]=%d,capacity[%d][%d]=%d flow[%d]=%d\n",front_index,front_index,flow[front_index],front_index,i,capacity[pre[i]][i],i,flow[i]);
myqueue.push(i);
}
}
}
//printf("\n");
if(pre[des]==-1) //残留图中不再存在增广路径
return -1;
else
return flow[des];
}
int maxFlow(int src,int des)
{
int increasement= 0;
int sumflow = 0;
while((increasement=BFS(src,des))!=-1)
{
int k = des; //利用前驱寻找路径
while(k!=src)
{
int last = pre[k];
capacity[last][k] -= increasement; //改变正向边的容量
capacity[k][last] += increasement; //改变反向边的容量
k = last;
}
sumflow += increasement;
}
return sumflow;
}
int main()
{
int i,j;
int start,end,ci;
while(cin>>n>>m)
{
memset(capacity,0,sizeof(capacity));
memset(flow,0,sizeof(flow));
for(i=0;i<n;++i)
{
cin>>start>>end>>ci;
if(start == end) //考虑起点终点相同的情况
continue;
capacity[start][end] +=ci; //此处注意可能出现多条同一起点终点的情况
}
cout<<maxFlow(1,m)<<endl;
}
return 0;
}