之前学完最大流问题后没及时整理,最近学离散数学涉及到这方面的知识,就回过头来复习一下,顺便整理下来。
首先,什么是最大流问题?
假设要把一些物品从结点S运送到结点T,可以借助其他结点进行中转,各结点之间的边的权值表示该边一次最多可以运输多少件物品,求最多可以一次性从结点S运输多少件物品到T。这就是最大流问题。
其中结点S称为源点(只流出不流入),结点T称为汇点(只流入不流出);
每条边的权值称为该边的容量(capacity),表示一次性能运输的物品数量,一般记为c(u,v);
每条边实际运输的物品数量称为流量(flow),一般记为f(u,v);其中 f(u,v)= -f(v,u),即把3个物品从u运输到v相当于把-3个物品从v运输到u (始终为u-3,v+3)
由于其他点只是用于中转,所以对除了源点S和汇点T外的任意结点v,Σf(u,v) = 0;因为要求最大流,所以所有中转点流入了多少物品就要流出多少物品,保证得到的结果是最大流。
最大流中容量和流量必须满足三个性质:
①容量限制:f(u,v)≤c(u,v);
②斜对称性:f(u,v) = -f(v,u);
③流量平衡:源点的流出量等于汇点的流入量,其他结点的流出量等于流入量。
Edmonds_Karp 算法
思想:初始时所有边的流量都为0,从0开始不断增流,即寻找一条可以从源点流向汇点的路径,并给该路径的所有边加上该路径可以通过的最大流量(即该路径上所有边的最小容量,这样才能满足上述的三个性质)。反复寻找这样的路径直到找不到这样的路径,那么我们就得到了该网络流的最大流啦!上述我们要找的路径称为增广路径。
每条边上的容量与流量的差值称为残余容量,简称残量,构成了一张残量网络。残量网络中的边最多可以为原图边数的两倍,因为残量网络中含有反向负权边。即原图中的f(u,v)所对应的-f(u,v)也存在与残量网络中。
此处的疑问是,为什么要有反向负权边?
我的理解是,这样的反向负权边实际上是一条容错的边,允许我们舍弃一开始找的增广路径,转而寻找更佳的增广路径的边。可以理解为回溯的另一种表示。用下面的图举例子,出自此博客,此博客也是我学习EK算法时感觉很不错的博客
我们第一次找到了1->2->3->4这条增广路;则残量网络变成下面的图;
若没有反向负权边,我们往下只能从1->3,然后就无法往下增广了,因为这时候3->4的边上已经没有剩余的容量了。结果得到的最大流是 1。但实际上我们可以通过1->2->4和1->3->4获得最大流2!
而有了反向负权边后,当我们走完1->3后,我们可以接着走3->2->4得到另一条增广路,最终得到最大流2,即正确答案。这好比我们对于第一次选择的路径不满意,重新走回去再走另一条路一样(是不是类似回溯?)
写到这的时候我自己突然产生一个疑问,这样的话只是把2->3的边往回走了啊,3->4的边还没往回走呢!再仔细思考一下,我得出了这样的答案:3->4的边没必要往回走,因为对于原图来说,结点3必有流出量,且必流向结点4!(也可以看作是原来的3->4现在是和1->3一起形成一条1->3->4的增广路)
好了,接下来说一下Edmonds_Karp 算法,此算法是基于BFS来求增广路径,最终求得最大流。
struct EdmondsKarp
{
//其中的maxn为const 值,根据不同题目的最大点数而设置
//其中 s 表示源点,t表示汇点
struct Edge
{
int from,to,cap,flow //边的起点、终点、容量、流量
Edge(int u,int v,int c,int f)from(u),to(v),cap(c),flow(f) {}
};
int n,m;//顶点数和边数
vector<Edge> edges;
vector<int> G[maxn];//G[i]记录以 i 为起点的边的序号
int r[maxn];// r[i]记录结点 i 到源点的可增广量
int p[maxn];// 记录增广路径
//初始化,清空图
void inti(int n)
{
for(int i=1; i<=n; ++i)
G[i].clear();
edges.clear();
}
//给图添加边
void addedge(int f,int t,int c)
{
edges.push_back(Edge(f,t,c,0));//原图的正向边
edges.push_back(Edge(t,f,0,0));//残量网络的反向边
int num = edges.size()-1;
G[f].push_back(num-1);
G[t].push_back(num);
}
//求最大流
int Maxflow()
{
int maxflow=0;
while(1)
{
memset(r,0,sizeof(r));
r[s] = INF;// INF为自己定义的无穷大量
queue<int> q;
q.push(s);
while(!q.empty())
{
int u=q.front();
q.pop();
//增加反向边不会对下面的for循环造成很大影响,因为这是BFS,是一层一层的搜索;
//反向边是下一层的点指向这一层的点,所以当遍历到反向边时,上一层的点已经搜索过了,所以会跳过。
//不过这里判断是否搜索过是根据r[node]是否为0判断的,即判断是否还可增广,可增广就继续往下。
for(int i=0; i<G[u].size(); ++i)
{
Edge e=edges[G[u][i]];
if(!r[e.to]&&e.cap>e.flow)
{
//寻找可增广量,所以取最小值;每次都从路径上的上一条边取最小,最终得最小
r[e.to]=min(r[u],e.cap-e.flow);
p[e.to]=G[u][i];
q.push(e.to);
}
}
//找到从汇点到源点的增广路
if(r[t])
break;
}
//当用BFS搜索完后从汇点到源点的可增广量为0,则已经求得最大流
if(!r[t])
break;
//更新残量网络图
for(int i=t; i!=s; i=edges[p[i]].from)
{
edges[p[i]].flow += r[t];
//反向边(这是我一开始学的时候的注释)
//为了让网络图能够遍历所有增广路径。如果没有反向边可能会导致一些路径没有搜索过;
//一般若是要一次搜索一条路径到底再回溯,这样复杂度会呈指数。
//所有用了反向边,当走过反向边时相当于把之前走过正向边时增加的流量减去,使得本来已经不能增流的边能再次通行。
//比如本来一个节点有多条连接的边,但因为某次遍历一条增广路径而使得下次遍历另一条路径时因为存在无法流通的边使得无法到达该节点,
//从而无法到达该节点所连接的其他的边,所以用反向边,使得本来无法流通的边可以流通(我觉得像是另一种方式的回溯,但没有那么高的复杂度)
//下面的p[i]^1保证了每次更新边的时候对相同的两个节点的正反边进行更新。
//因为根据正反边的存放顺序,若p[i]保存的是正边(则p[i]为偶数),则反边为p[i]+1=p[i]^1;
//若p[i]保存的是反边(则p[i]为奇数),则正边p[i]-1=p[i]^1。
edges[p[i]^1].flow -= r[t]; //根据性质 f(u,v) = -f(v,u)
}
maxflow += r[t];
}
return maxflow;
}
};
上述代码为紫书的代码,再添加我个人的一些注释,希望能帮助大家更好的理解。
代码中的r[i]数组当i被搜索过后都会变为正值(除非没有已经没有增广路),所以同时用他作为标记数组记录访问过的点。
以及紫书上,刘汝佳大神的话:实践中一般不用Edmonds_Karp 算法,而是用效率更高的Dinic算法或者ISAP算法(比较难理解),建议是理解Edmonds_Karp 算法的原理,但比赛中使用Dinic或者ISAP算法。
个人觉得对于不搞竞赛的学生,理解EK算法的原理就已经不错了,这个算法为我们提供了一种寻找增广路径的思路,这种思路我相信可以适用于其他问题,比如我之前写过的匈牙利算法。上述的两个比较难但高效的算法等有空有兴趣再学吧,先到这了。