1.流网络
流网络G=(V,E)是一个有向图,图中每条边(u,v) in E有一个非负的容量值 c(u,v) >= 0。而且如果边集合E包含一条边(u,v),则图中不存在反向边(v,u)。在流网络的所有节点中,比较特殊的是源节点s和汇节点t,为了方便起见,假定每个节点都在源节点到汇节点的路径上,也就是说,流网络图是连通的。对于流,比较重要的两条性质是:容量限制和流量守恒,流f的值是从源节点流出的总流量减去流入源节点的总流量。
另外一个问题,如果图中出现了边(v1,v2)和(v2,v1)(称为反平行),这就违反了上面的假设,可以通过加入新节点的方式将其转换为等价的不包含反平行边的网络。
对于多源点多汇点的网络,我们可以通过加入一个超级源节点s 和 超级汇点t 的方式解决。
2.Ford-Fulkerson方法
关于Ford-Fulkerson方法的运行方式可以通过下面的伪代码加以说明
FORD-FULKERSON_METHOD(G,s,t)
1. initialize flow f to 0
2. while there exists an augmenting path p in the residual network G_f
3. augment flow f along p
4. return f
为了更好的理解Ford-Fulkerson方法,需要理解下面的几个概念
2.1 残存网络
从直观上看,给定流网络G和流量f,残存网络G_f由那些仍然有空间对流量进行调整的边构成。残存网络G_f还可能包含图G中不存在的边,比如为了表示对一个正流量f(u,v)的缩减,我们将边(v,u)加入到图G_f中,并将其残存容量设置为c_f(u,v) = f(u,v)。
如果f是G的一个流,f‘是对应的残存网络G_f中的一个流,定义(f /|\ f')为流f'对流f的递增,定义如下:
(1)
引理1 设G=(V,E)为一个流网络,源节点为s,汇节点为t,设f为G中一个流。设G_f为由流f所诱导的G的残存网络,设f'为G_f的一个流。那么上面式子中所定义的函数(f/|\f')是G的一个流,其值为|f/|\f'| = |f| + |f'|。
2.2. 增广路径
给定流网络G=(V,E)和流f,增广路径p是残存网络G_f中从源节点s到汇点t的简单路径。
称在一条增广路径p上能够为每条边增加的流量的最大值为路径p的残存容量,由下面的表达式给出:
c_f(p) = min{c_f(u,v):(u,v)属于路径p}
引理2:设 G = (V,E)为一个流网络,设f为图G的一个流,设p为残存网络G_f中的一条增广路径。定义一个函数f_p: V X V - -> R如下:
(2)
则f_p是残存网络G_f中的一个流,其值为|f_p| = c_f(p) > 0。
推论1:设G=(V,E)为一个流网络设f为G中的一个流,设p为残存网络G_f中的一条增广路径。设f_p由上面公式(2)定义,假定将f增加f_p的量,则函数(f /|\ f_p)是图G中的一个流,其值为|f/|\f_p| = |f| + |f_p| > |f|。
2.3流网络切割
流网络G = (V,E)的一个切割(S,T)将节点集合V划分为S和 T = V-S,使得s in S,t in T。关于切割的净流量以及切割的容量的公式分别如下:
引理3: 设f为流网络G的一个流,该流网络的源节点为s,汇点为t,设(S,T)为流网络G的任意切割,则横跨切割(S,T)的净流量为f(S,T) = |f|。
推论2:流网络G中任意流f的值不能超过G的任意一个切割的容量。
定理1(最大流最小切割定理):设f为流网络G = (V,E)中的一个流,该流网络的源节点为s,汇节点为t,则下面的条件是等价的:
1. f是G的一个最大流
2. 残存网络G_f不包括任何增广路径
3. |f| = c(S,T),其中(S,T)是流网络G的某个切割
3.Ford-Fulkerson算法
伪代码:
FORD-FULKERSON(G,s,t)
1. for each edge(u,v) in G.E
2. (u,v).f = 0
3. while there exists a path p from s to t in the residual network G_f
4. c_f(p) = min{c_f(u,v):(u,v) is in p}
5. for each edge(u,v) in p
6. if (u,v) in E
7. (u,v).f = (u,v).f + c_f(p)
8. else (v,u).f = (v,u).f - c_f(p)
算法运行时间取决于如何寻找增广路径。
假设 f* 表示转换后网络的一个最大流,则在FORD-FULKERSON的一个直接实现中,执行3-8行的while最多次数是|f*|次,因为流量直每次迭代中最少增加一个单位。如果使用深度优先或广度优先搜索,在一个残存网络中找到一条路径的时间为O(V+E') = O(E)。所以while循环的每一次执行时间为O(E),这与1-2行初始化的时间相同,因此整个FORD-FULKERSON算法的时间复杂度为O(E|f*|)。
Java实现的代码参考后面的文章《Ford-Fulkerson算法 java实现》
4.Edmonds-Karp算法
可以通过广度优先搜索来改善FORD-FULKERSON算法的效率,如此实现的Ford-Fulkerson方法为Edmonds-Karp算法。时间复杂度为O(VE^2)。
下面是对时间复杂度的分析,下面引理用delta_f(u,v)来表示残存网络G_f中从节点u到节点v的最短路径距离,其中每条边的权重为单位距离。
引理1:如果Edmonds-Karp算法运行在流网络G = (V,E)上,该网络的源节点为s汇点为t,则对于所有的节点v in V - {s,t},残存网络G_f中的最短路径距离delta_f(s,v)随着每次流量的递增而单调递增。
定理1:如果Edmonds-Karp算法运行在源节点为s汇点为t的流网络G = (V,E)上,则该算法所执行的流量递增操作的总次数为O(VE)。
由于在用广度优先搜索寻找增广路径时,FORD-FULKERSON中的每次迭代可以在O(E)时间内实现,所以Edmonds-Karp的总运行时间为O(VE^2)。
下面是该算法的实现(这个不是我写的,之前保存在草稿箱里,出处忘了,侵删)
#include <iostream>
#include <queue>
#include <algorithm>
using namespace std;
const int msize = 205;
int N, M; //N--路径数, M--结点数
int r[msize][msize]; // 流网络,值为流量限制
int pre[msize]; // 记录结点i的前向结点为pre[i]
bool vis[msize]; // 记录结点i是否已访问
// 用BFS判断s,t之间是否还有增广路径,若有,返回1
// 时间复杂度O(V+E) 用邻接表时
bool BFS(int s, int t)
{
queue<int> que;
memset(pre, -1, sizeof(pre));
memset(vis, false, sizeof(vis));
pre[s] = s;
vis[s] = true;
que.push(s);
int p;
while(!que.empty())
{
p = que.front();
que.pop();
for(int i=1; i<=M; ++i)
{
if(r[p][i]>0 && !vis[i])
{
pre[i] = p;
vis[i] = true;
if(i == t) // 存在增广路径
return true;
que.push(i);
}
}
}
return false;
}
//O(VE^2)
int EK(int s, int t)
{
int maxflow = 0, d;
while(BFS(s, t)) //存在增广路径O(VE)
{
d = INT_MAX;
// 若有增广路径,则找出最小的delta
for(int i=t; i!=s; i=pre[i])
d = min(d, r[pre[i]][i]);
// O(E)
for(int i=t; i!=s; i=pre[i])
{
r[pre[i]][i] -= d;
r[i][pre[i]] += d;
}
maxflow += d;
}
return maxflow;
}
int main()
{
while(cin >> N >> M)
{
memset(r, 0, sizeof(r));
int s, e, c;
for(int i=0; i<N; ++i)
{
cin >> s >> e >> c;
r[s][e] += c;
}
cout << EK(1, M) << endl;
}
return 0;
}