最大流之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方法首先對圖中的所有邊的流量初始化為零值,然後開始進入迴圈:如果在殘存網路中可以找到一條從s到t的增廣路徑,那麼要找到這條這條路徑上值最小的邊,然後根據該值來更新流網路。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有很多種實現,主要不同點在於如何尋找增廣路徑。最開始提出該方法的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一下哦~ 由於本人還在研究《演算法導論》,所以這個專案還會持續更新哦~ 大家一起好好學習~