1. 程式人生 > >最大流之Ford-Fulkerson方法詳解及實現

最大流之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一下哦~ 由於本人還在研究《演算法導論》,所以這個專案還會持續更新哦~ 大家一起好好學習~