最大流之Ford-Fulkerson方法详解及实现

最大流问题常常出现在物流配送中,可以规约为以下的图问题。最大流问题中,图中两个顶点之间不能同时存在一对相反方向的边。

边上的数字为该条边的容量,即在该条边上流过的量的上限值。最大流问题就是在满足容量限制条件下,使从起点s到终点t的流量达到最大。在介绍解决最大流问题的Ford-Fulkerson方法之前,先介绍一些基本概念。

1.  残存网络与增广路径

根据图和各条边上的流可以画出一幅图的残存网络如下所示。左图为流网络,右图为残存网络,其中流网络中边上的数字分别是流量和容量,如10/12,那么10为边上的流量,12为边的容量。残存网络中可能会存在一对相反方向的边,与流网络中相同的边代表的是流网络中该边的剩余容量,在流网络中不存在的边代表的则是其在流网络中反向边的已有流量,这部分流量可以通过“回流”减少。例如,右图残存网络中,边<s,v1>的剩余容量为4,其反向边<v1.s>的值为12,即左图流网络中的边<s,v1>的流量。在残存网络中,值为0的边不会画出,如边<v1,v2>。

残存网络描述了图中各边的剩余容量以及可以通过“回流”删除的流量大小。在Ford-Fulkerson方法中,正是通过在残存网络中寻找一条从s到t的增广路径,并对应这条路径上的各边对流网络中的各边的流进行修改。如果路径上的一条边存在于流网络中,那么对该边的流增加,否则对其反向边的流减少。增加或减少的值是确定的,就是该增广路径上值最小的边。

2. Ford-Fulkerson方法

Ford-Fulkerson方法的正确性依赖于这个定理:当残存网络中不存在一条从s到t的增广路径,那么该图已经达到最大流。这个定理的证明及一些与其等同的定理可以参考《算法导论》。

Ford-Fulkerson方法的伪代码如下。其中<u,v>代表顶点u到顶点v的一条边,<u,v>.f表示该边的流量,c是边容量矩阵,c(i,j)表示边<i,j>的容量,当边<i,j>不存在时,c(i,j)=0。e为残存网络矩阵,e(i,j)表示边<i,j>的值,当边<i,j>不存在时,e(i,j)=0。E表示边的集合。f表示流网络。

Ford-Fulkerson

    for <u,v> ∈ E

        <u,v>.f = 0

    while find a route from s to t in e

        m = min(<u,v>.f, <u,v>  ∈ route)

        for <u,v> ∈ route

            if <u,v>  ∈ f

                <u,v>.f = <u,v>.f + m

            else

                <v,u>.f = <v,u>.f - m
Ford-Fulkerson方法首先对图中的所有边的流量初始化为零值,然后开始进入循环:如果在残存网络中可以找到一条从s到t的增广路径,那么要找到这条这条路径上值最小的边,然后根据该值来更新流网络。

Ford-Fulkerson有很多种实现,主要不同点在于如何寻找增广路径。最开始提出该方法的Ford和Fulkerson同学在其论文中都是使用广度优先搜索实现的,其时间复杂度为O(VE),整个算法的时间复杂度为O(VE^2)。

下面我给出一个应用Bellman-Ford计算单源最短路径的算法实现寻找一条增广路径,对于用邻接矩阵表示的图来说,该实现的时间复杂度为O(V^3),对于用邻接表表示的图来说,时间复杂度则为O(VE)。

// 寻找增广路径
int findRoute(int **e, int vertexNum, int *priorMatrix, int s,int t)
{
	s--; t--;
	int *d = (int *)malloc(sizeof(int)*vertexNum);
	// initialize
	for (int i = 0; i < vertexNum; i++)
	{
		d[i] = 0;
		priorMatrix[i] = -1;
	}
	d[s] = 1;
	// 反复用边<i,j>做松弛操作,将<s,...,j>更新为<s,...,i,j>
	for (int k = 0; k < vertexNum; k++)
	{
		for (int i = 0; i < vertexNum; i++)
		{
			for (int j = 0; j < vertexNum; j++)
			{
				if (d[j] == 0)
				{
					d[j] |= (d[i] & (*((int*)e + i*vertexNum + j) > 0));
					if (d[j] == 1)
					{
						priorMatrix[j] = i;
					}
				}
			}
		}
	}
	if (d[t] == 0)	return 0;

	int min = INT_MAX;
	int pre = priorMatrix[t];
	while (pre != -1)
	{
		if (min > *((int*)e + pre*vertexNum + t))
		{
			min = *((int*)e + pre*vertexNum + t);
		}
		t = pre;
		pre = priorMatrix[t];
	}
	return min;
}
该实现应用了计算图的最短路径方法中的思想,对图中的边反复在松弛操作,从而计算得到一个源点到其它所有点的路径。这里我们不需要计算最短路径,只要找到一条可行路径即可。上述findRoute方法的实现原理可以参考我前面的一篇文章 单源最短路径之Bellman-Ford算法 。在寻找路径的同时,我们还要记录一个前驱子图priorMatrix,其本质上是一个一位数组,其记录了从顶点s到其它顶点的一条可行路径上的终点的前一个顶点。于是我们就可以从前驱子图中找到从s到t的一条完整路径。其正确性由图的最短路径的计算方法思想保证。具体可以参考我另一篇博客 结点对最短路径之Floyd算法详解及实现

下面给出根据图和流网络计算残存网络的代码。

// 计算残存网络
void calculateENet(int **c, int vertexNum, int **f, int **e)
{
	for (int i = 0; i < vertexNum; i++)
	{
		for (int j = 0; j < vertexNum; j++)
		{
			int a = *((int*)c + i*vertexNum + j);
			if (a != 0)
			{
				*((int*)e + i*vertexNum + j) = a - *((int*)f + i*vertexNum + j);
				*((int*)e + j*vertexNum + i) = *((int*)f + i*vertexNum + j);
			}
			else
			{
				*((int*)e + i*vertexNum + j) = 0;
			}
		}
	}
}
下面给出Ford-Fulkerson方法的实现代码。

/**
* Ford-Fulkerson方法的一种实现
* @param c 二维矩阵,记录每条边的容量
* @param vertexNum 顶点个数,包括起点和终点
* @param s 起点编号,编号从1开始
* @param t 终点编号
* @param f 输出流网络矩阵,二维矩阵,记录每条边的流量
*/
void Ford_Fulkerson(int **c, int vertexNum, int s, int t, int **f)
{
	int *e = (int *)malloc(sizeof(int)*vertexNum*vertexNum);	// 残存网络
	int *priorMatrix = (int *)malloc(sizeof(int)*vertexNum);	// 增广路径的前驱子图

	// initialize
	for (int i = 0; i < vertexNum;i++)
	{
		for (int j = 0; j < vertexNum; j++)
		{
			*(f + i*vertexNum + j) = 0;
		}
	}

	while (1)
	{
		calculateENet(c, vertexNum, (int **)f, (int **)e);	// 计算残存网络
		int min;
		if ((min = findRoute((int **)e, vertexNum, priorMatrix, s, t)) == 0)	// 寻找增广路径及其最小流值
		{
			break;
		}
		int pre = priorMatrix[t - 1];
		int next = t - 1;
		while (pre != -1)		// 按增广路径更新流网络
		{
			if (*((int*)c + pre * vertexNum + next) != 0)
			{
				*((int*)f + pre * vertexNum + next) += min;
			}
			else
			{
				*((int*)f + next * vertexNum + pre) -= min;
			}
			next = pre;
			pre = priorMatrix[pre];
		}
	}
}

3. 测试及效果

下面给出用于测试的代码。

void testFord()
{
	int c[6][6] = {	0,		16,		13,		0,		0,		0,
					0,		0,		0,		12,		0,		0,
					0,		4,		0,		0,		14,		0,
					0,		0,		9,		0,		0,		20,
					0,		0,		0,		7,		0,		4,
					0,		0,		0,		0,		0,		0	};
	int f[6][6];
	Ford_Fulkerson((int **)c, 6, 1, 6, (int **)f);
	for (int i = 0; i < 6; i++)
	{
		for (int j = 0; j < 6; j++)
		{
			int flow = f[i][j];
			if (flow != 0)
			{
				printf("%d -> %d : %d\n", i + 1, j + 1, flow);
			}
		}
	}
}
上述代码构造的图如下所示。

运行结果如下,其中1为顶点s,5为顶点t,2~5依次为顶点v1、v2、v3和v4。

流网络和残存网络如下所示,其中左图为流网络,右图为残存网络。

我们可以看到残存网络中的确已经不存在一条从s到t的路径了。此时Ford-Fulkerson方法的循环应该终止,最大流量为各边的流量相加之和,即76。

完整的程序可以看到我的github项目 数据结构与算法

这个项目里面有本博客介绍过的和没有介绍的以及将要介绍的《算法导论》中部分主要的数据结构和算法的C实现,有兴趣的可以fork或者star一下哦~ 由于本人还在研究《算法导论》,所以这个项目还会持续更新哦~ 大家一起好好学习~
  • 21
    点赞
  • 82
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
Ford-Fulkerson算法是求解最大流问题的一种经典算法。以下是一个基于增广路思想的Ford-Fulkerson算法的代码: ``` // 基于邻接矩阵的Ford-Fulkerson算法实现 #include <iostream> #include <queue> #include <cstring> using namespace std; const int MAXN = 100; // 最大顶点数 const int INF = 0x3f3f3f3f; // 表示无穷大 int n, m; // n表示顶点数,m表示边数 int s, t; // s表示源点,t表示汇点 int cap[MAXN][MAXN]; // 表示容量 int flow[MAXN][MAXN]; // 表示流量 int pre[MAXN]; // 表示前驱节点 int bfs() { memset(pre, -1, sizeof(pre)); // 初始化前驱节点数组 queue<int> q; q.push(s); pre[s] = -2; while (!q.empty()) { int u = q.front(); q.pop(); for (int v = 0; v < n; ++v) { if (pre[v] == -1 && cap[u][v] > flow[u][v]) { pre[v] = u; if (v == t) return 1; q.push(v); } } } return 0; } int maxFlow() { int ans = 0; while (bfs()) { int minflow = INF; for (int u = t; u != s; u = pre[u]) { int v = pre[u]; minflow = min(minflow, cap[v][u] - flow[v][u]); } for (int u = t; u != s; u = pre[u]) { int v = pre[u]; flow[v][u] += minflow; flow[u][v] -= minflow; } ans += minflow; } return ans; } int main() { cin >> n >> m >> s >> t; memset(cap, 0, sizeof(cap)); memset(flow, 0, sizeof(flow)); for (int i = 0; i < m; ++i) { int u, v, c; cin >> u >> v >> c; cap[u][v] += c; // 注意有可能存在重边 } cout << maxFlow() << endl; return 0; } ``` 算法思路: 1. 初始化流量为0; 2. 在剩余容量大于0的情况下,寻找增广路: - 从源点s开始,使用BFS寻找一条增广路; - 如果找到增广路,计算增广路上的最小剩余容量minflow,更新流量; 3. 最大流就是所有增广路上的最小剩余容量之和。 其中,增广路的定义是指从源点到汇点路径上,剩余容量均大于0的路径。在Ford-Fulkerson算法中,每次都需要寻找一条增广路来更新流量,直到无法再找到增广路为止。这个过程中,每次找到的增广路都可以使得流量增加,因此最终的流量是不断增加的。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值