這篇文章我們將重溫最大流問題,實現一些最有名的增廣路徑算法的實際分析的目標。我們將討論的這幾種算法的複雜度在O(n*m*m)到O(n*mlogU)之間,並且從討論的結果中得到在實踐中最有效的一種。正如我們所想的,理論上的複雜度並不能揭示該算法在實際中的價值。
這篇文章所針對的是熟悉網絡流理論的基本知識的讀者。如果你對網絡流理論的基本知識不是很瞭解的話,我會建議你先看參考文獻[1]、參考文獻[2]以及參考文獻[5]---算法教程之最大流問題。
在第一節,將涉及到最大流理論的一些必要的定義和聲明。在中間的章節,我們將會着重討論增光路徑算法。在最後一節,將會展示時間分析的結果,並強調在實踐中的最佳算法,同時我也會給出該算法的簡單實現。
第一節 最大流問題的聲明
假設一有向網G = (V, E),其中V表示頂點集,E表示邊集。相關聯的兩個節點i和j所組成的弧arc(i,j)均有非負的容量Uij。同時我們在有向網G中定義了兩個特殊的頂點,即一個源點s和一個匯點t。
對於V集中的節點i,我們用E(i)表示從節點i產生的所有邊。
令U = max Uij。
令n表示頂點的個數,m表示邊的個數。
我們期望從源點s到匯點t之間找到這個最大流,並且在所有節點中滿足:從一個頂點到另一個頂點的流不能超過設定的容量。用Xij代表邊arc(i,j)的流,那麼我們就能得到最大流問題優化的模型:
如下圖:
Xij被稱爲一個可行的解決方案或者可行流,並且它滿足所有的約束條件。給一個流x,我們能根據以下的想法來構造出殘留網絡。假設邊(i,j)是流中的單元Xij,那麼我們定義邊(i,j)的殘留容量Rij = Uij - Xij。這就意味着我們可以從頂點i到頂點j壓入額外的單位流量Rij。如果我們從j到i的弧(i,j)壓入Xij單位流量,就可以抵消從i到j的流Xij。
因此,給定一個可行流x,我們定義流的殘留網絡x如下:假設一網絡G = (V, E),一個可行的解決方案x可產生一個新的殘留網絡,我們用Gx = (V, Ex)來定義這個殘留網絡,其中,Ex是一個可行的解決方案對應的殘留邊的集合x。
附註:殘留的容量 + 反向平衡的流量共同構成了殘留網絡。
那什麼是Ex呢?我們用邊(i,j)、邊(j,i)來代表弧(i,j):邊(i,j)的殘留容量Rij = Uij - Xij,並且邊(j,i)的殘留容量Rij = Xij。然後我們就能從一個正的殘留容量的新邊集中構造集合Ex。
第二節 增廣路徑算法作爲一個整體
在這節中,我將描述一種構造所有增廣路徑算法的方法,這種方法是由Ford and Fulkerson在1956年發明的。
增廣路徑是找出在殘留網絡中從源點到匯點的有向路徑。增廣路徑的殘留容量是路徑中任意邊所形成的最小殘留容量。顯然,我們可以沿着增廣路徑從源點到匯點發送額外的流。
假如有這麼一條路,這條路從源點開始一直一段一段的連到了匯點,並且,這條路上的每一段都滿足流量 < 容量。那麼,我們一定能找到這條路上的每一段的(容量-流量)的值當中的最小值delta。我們把這條路上每一段的流量都加上這個delta,一定可以保證這個流依然是可行流。這樣我們就得到了一個更大的流,他的流量是之前的流量+delta,而這條路就叫做增廣路徑。
所有的增廣路徑算法的構造是基於增廣路徑定理的:
定理一(增廣路徑定理):流x是最大流當且僅當這個殘留網絡不包含其他增廣路經。
由這個定理我們得到一種找到最大流的方法。這種方法通過在所有路徑中不斷地找出增廣路徑和增廣流,直到網絡中不在包含這樣的路徑。我們要討論的一些算法,它們所不不同的只是尋找增廣路徑的方法。
我們認爲最大流問題基於以下假設:
假設一:這個流網絡是一個有向網。
假設二:網絡中的所有容量都是非負整數。
附註:這個假設對於某些算法不是必須的,這些算法的複雜邊界涉及到數據的完整性。
假設三:這個問題有一個最佳解決方案,且這個方案是有界的。
附註:這個特定的假設意味着從源點到匯點是有容量限制的路徑。
假設四:這個網中不包含平行的弧。
附註:這個假設的規定不失一般性,因爲我們可以總結出所有平行弧的容量。
至於這些假設爲什麼是正確的,我將證明留給讀者自己。
其實我們很容易確定上述辦法的正確性。根據假設二,對於每一增廣步驟,我們增加流值至少一個單位流量,通常開始的流值是0。最大流的值從上文中得知其是有界的,根據假設三。而這種推理表明了該方法的有限性。
有了以上的這些準備,我們開始討論算法。
第三節 最短增廣路徑算法,O(n*n*m)
Edmonds and Karp在1972年,以及Dinic在1970年都獨立的證明了如果每步增廣路徑都是最短的話,那麼整個算法將會執行O(n*m)步。之所以能實現這個最短路徑算法(每條邊的長度等於一)是利用了廣度優先搜索算法BFS的,參考文獻[2]、參考文獻[6].增廣路徑算法被廣泛的討論和研究在許多書籍和文章中,包括參考文獻[5]。我們回顧一下此算法:
如下圖:
這篇文章所針對的是熟悉網絡流理論的基本知識的讀者。如果你對網絡流理論的基本知識不是很瞭解的話,我會建議你先看參考文獻[1]、參考文獻[2]以及參考文獻[5]---算法教程之最大流問題。
在第一節,將涉及到最大流理論的一些必要的定義和聲明。在中間的章節,我們將會着重討論增光路徑算法。在最後一節,將會展示時間分析的結果,並強調在實踐中的最佳算法,同時我也會給出該算法的簡單實現。
第一節 最大流問題的聲明
假設一有向網G = (V, E),其中V表示頂點集,E表示邊集。相關聯的兩個節點i和j所組成的弧arc(i,j)均有非負的容量Uij。同時我們在有向網G中定義了兩個特殊的頂點,即一個源點s和一個匯點t。
對於V集中的節點i,我們用E(i)表示從節點i產生的所有邊。
令U = max Uij。
令n表示頂點的個數,m表示邊的個數。
我們期望從源點s到匯點t之間找到這個最大流,並且在所有節點中滿足:從一個頂點到另一個頂點的流不能超過設定的容量。用Xij代表邊arc(i,j)的流,那麼我們就能得到最大流問題優化的模型:
如下圖:
Xij被稱爲一個可行的解決方案或者可行流,並且它滿足所有的約束條件。給一個流x,我們能根據以下的想法來構造出殘留網絡。假設邊(i,j)是流中的單元Xij,那麼我們定義邊(i,j)的殘留容量Rij = Uij - Xij。這就意味着我們可以從頂點i到頂點j壓入額外的單位流量Rij。如果我們從j到i的弧(i,j)壓入Xij單位流量,就可以抵消從i到j的流Xij。
因此,給定一個可行流x,我們定義流的殘留網絡x如下:假設一網絡G = (V, E),一個可行的解決方案x可產生一個新的殘留網絡,我們用Gx = (V, Ex)來定義這個殘留網絡,其中,Ex是一個可行的解決方案對應的殘留邊的集合x。
附註:殘留的容量 + 反向平衡的流量共同構成了殘留網絡。
那什麼是Ex呢?我們用邊(i,j)、邊(j,i)來代表弧(i,j):邊(i,j)的殘留容量Rij = Uij - Xij,並且邊(j,i)的殘留容量Rij = Xij。然後我們就能從一個正的殘留容量的新邊集中構造集合Ex。
第二節 增廣路徑算法作爲一個整體
在這節中,我將描述一種構造所有增廣路徑算法的方法,這種方法是由Ford and Fulkerson在1956年發明的。
增廣路徑是找出在殘留網絡中從源點到匯點的有向路徑。增廣路徑的殘留容量是路徑中任意邊所形成的最小殘留容量。顯然,我們可以沿着增廣路徑從源點到匯點發送額外的流。
假如有這麼一條路,這條路從源點開始一直一段一段的連到了匯點,並且,這條路上的每一段都滿足流量 < 容量。那麼,我們一定能找到這條路上的每一段的(容量-流量)的值當中的最小值delta。我們把這條路上每一段的流量都加上這個delta,一定可以保證這個流依然是可行流。這樣我們就得到了一個更大的流,他的流量是之前的流量+delta,而這條路就叫做增廣路徑。
所有的增廣路徑算法的構造是基於增廣路徑定理的:
定理一(增廣路徑定理):流x是最大流當且僅當這個殘留網絡不包含其他增廣路經。
由這個定理我們得到一種找到最大流的方法。這種方法通過在所有路徑中不斷地找出增廣路徑和增廣流,直到網絡中不在包含這樣的路徑。我們要討論的一些算法,它們所不不同的只是尋找增廣路徑的方法。
我們認爲最大流問題基於以下假設:
假設一:這個流網絡是一個有向網。
假設二:網絡中的所有容量都是非負整數。
附註:這個假設對於某些算法不是必須的,這些算法的複雜邊界涉及到數據的完整性。
假設三:這個問題有一個最佳解決方案,且這個方案是有界的。
附註:這個特定的假設意味着從源點到匯點是有容量限制的路徑。
假設四:這個網中不包含平行的弧。
附註:這個假設的規定不失一般性,因爲我們可以總結出所有平行弧的容量。
至於這些假設爲什麼是正確的,我將證明留給讀者自己。
其實我們很容易確定上述辦法的正確性。根據假設二,對於每一增廣步驟,我們增加流值至少一個單位流量,通常開始的流值是0。最大流的值從上文中得知其是有界的,根據假設三。而這種推理表明了該方法的有限性。
有了以上的這些準備,我們開始討論算法。
第三節 最短增廣路徑算法,O(n*n*m)
Edmonds and Karp在1972年,以及Dinic在1970年都獨立的證明了如果每步增廣路徑都是最短的話,那麼整個算法將會執行O(n*m)步。之所以能實現這個最短路徑算法(每條邊的長度等於一)是利用了廣度優先搜索算法BFS的,參考文獻[2]、參考文獻[6].增廣路徑算法被廣泛的討論和研究在許多書籍和文章中,包括參考文獻[5]。我們回顧一下此算法:
如下圖:
在第五行,把沿P的流加上其殘留容量。
該算法通過執行O(n*m)步以找出一條增廣路徑。由於在廣度優先搜索時最壞情況下需O(m)次操作,所有算法的總複雜度應是O(n*m*m)。我會在下面舉個簡單的例子。
第四節 改進的最短增廣路徑算法,O(n*n*m)
如我們早先提到的,找到任意最短增廣路徑的方法就是在殘留網絡中,通過執行廣度優先搜索來找到這些路徑。在最壞情況下BFS需要O(m)次操作以及規定最大流的時間複雜度爲O(n*n*m)。於是在1987年,Ahuja和Orlin改進了最短增廣路徑算法,參考文獻[1]。他們利用這一事實:在所有增廣中,從頂點i到匯點t的最小距離是單調遞增的,並且將每次增廣的平均時間減少到O(n)。改進後的增廣路徑算法,運行的時間仍然是O(n*n*m)。現在我們就可以根據參考文獻[1]來討論它了。
定義一:距離函數d
殘留容量Rij表示的是一個從節點集到非負整數的函數。如果距離函數滿足一下的幾個條件,那麼我們就說它是有效的。
● d(t) = 0;
● d(i) <= d(j) + 1,且Rij > 0
很容易證明,在殘留網絡Gx中,從某一頂點i到匯點t,節點i的有效距離標號(由d(i)表示)是最短路徑長度的下界。若殘量網絡中任意一點的距離標號正好等於該頂點至匯點t的最短路路長,則稱距離函數是精確的。同時我們也很容易的證明,如果d(s) >= n,那麼殘留網絡中不再有從源點到匯點的路徑。
如果滿足d(i) = d(j) + 1的邊,我們稱這條邊(i,j)是可容許的,反之,其他的邊是不容許的。如果一條路徑包含了從源點s到匯點t包含了可容許的邊,那麼這條路徑是可容許的。顯然,一條了容許的路徑是從源點到匯點的最短路徑。對於可容許路徑中的每一條邊需滿足條件Rij > 0,它是一條增廣路徑。
因此,改進後的最短增廣路徑算法包括四步:main cycle, advance, retreat and augment。如下圖所示:
該算法通過執行O(n*m)步以找出一條增廣路徑。由於在廣度優先搜索時最壞情況下需O(m)次操作,所有算法的總複雜度應是O(n*m*m)。我會在下面舉個簡單的例子。
第四節 改進的最短增廣路徑算法,O(n*n*m)
如我們早先提到的,找到任意最短增廣路徑的方法就是在殘留網絡中,通過執行廣度優先搜索來找到這些路徑。在最壞情況下BFS需要O(m)次操作以及規定最大流的時間複雜度爲O(n*n*m)。於是在1987年,Ahuja和Orlin改進了最短增廣路徑算法,參考文獻[1]。他們利用這一事實:在所有增廣中,從頂點i到匯點t的最小距離是單調遞增的,並且將每次增廣的平均時間減少到O(n)。改進後的增廣路徑算法,運行的時間仍然是O(n*n*m)。現在我們就可以根據參考文獻[1]來討論它了。
定義一:距離函數d
殘留容量Rij表示的是一個從節點集到非負整數的函數。如果距離函數滿足一下的幾個條件,那麼我們就說它是有效的。
● d(t) = 0;
● d(i) <= d(j) + 1,且Rij > 0
很容易證明,在殘留網絡Gx中,從某一頂點i到匯點t,節點i的有效距離標號(由d(i)表示)是最短路徑長度的下界。若殘量網絡中任意一點的距離標號正好等於該頂點至匯點t的最短路路長,則稱距離函數是精確的。同時我們也很容易的證明,如果d(s) >= n,那麼殘留網絡中不再有從源點到匯點的路徑。
如果滿足d(i) = d(j) + 1的邊,我們稱這條邊(i,j)是可容許的,反之,其他的邊是不容許的。如果一條路徑包含了從源點s到匯點t包含了可容許的邊,那麼這條路徑是可容許的。顯然,一條了容許的路徑是從源點到匯點的最短路徑。對於可容許路徑中的每一條邊需滿足條件Rij > 0,它是一條增廣路徑。
因此,改進後的最短增廣路徑算法包括四步:main cycle, advance, retreat and augment。如下圖所示:
在retreat步驟的第一行,如果Ex(i)是空的,此時假設d(i)=n。
該算法保留部分可容許的路徑。比如,從源點s到某一頂點i包含了容許邊。算法從部分可容許路徑的末節點(這類節點也稱當前節點)開始執行advance或者retreat步驟。如果從當前節點始發有一些可容許的邊,那麼將會執行算法的advance步驟,並會將這條邊添加到部分可容許的邊中。否則,算法將會執行retreat步驟。
如果部分容許路徑到達了匯點,我們就執行一次增廣。當d(s) >= n是算法結束。另外,Ex(i)的正規表達式:Ex(i) = { (i,j) in E(i): Rij > 0 }。
現在我大概證明一下該算法的運行時間O(n*n*m)。
引理一:算法每步都會保存距離標號。此外,每次重標號都要嚴格地增加一個節點的距離標號。
證明描述:對一些重標號操作和增廣執行歸納法。
引理二:每個節點的距離標號之多增添n次。連續的,重標號操作至多執行n*n次。
證明:引理二是引理一的延伸,如果d(s) >= n,那麼殘留網絡中不在包含增廣路徑。
因爲改進的最短增廣路徑算法產生增廣是沿着最短路徑(和沒有改進的算法一樣),所以增廣的總數都是相同的O(n*m)。執行一次retreat步就重標一個節點,這就是爲什麼retreat steps需要O(n*n)(根據引理二)。執行retreat/relabel步驟的時間是O( n ∑i in V |E(i)| ) = O(nm)。由於一次增廣需要時間O(n),所以總的增廣時間應是O(n*n*m)。advance步驟執行的總時間是增廣時間加上retreat/relabe的時間,也是O(n*n*m)。於是我們得到以下定理:
定理二:改進後的最短增廣路徑算法的運行時間爲O(n*n*m)。
Ahuja and Orlin認爲這是對該算法非常實用的一次改進。因爲當最大流被找到的時候,算法執行了許多無用的重標號操作,解決無效操作更好的辦法就是添加一個終止的條件。我們引入一個(n+1)維的數組numbs,下標從0到n。numbs(k)代表的值是節點的個數,它的參數k等於距離標號。當算法利用BFS計算初始距離標號的同時,初始化這個數組numbs。
當算法從節點x到節點y增加節點的距離標號時,將會從numbs(x)減1,而numbs(y)加1,同時檢查numbs(x)是否等於0。如果等於0,算法終止。
這種方法是一種啓發式,但是它在實際中確實很好用。證明留給讀者。(提示:當節點i有d(i) > x,以及節點j有d(j) < x時產生割,此時就要利用最大流最小割定理。)
第五節 改進後的算法和沒改進算法的比較
本節,我們將在最壞情況下來比較兩種最短增廣路徑算法的運行時間。
最壞情況下,不論是改進的還是沒有改進的算法都會執行O(n*n*n)次。如果m = n*n,Norman Zade開發了一些基於運行時間的例子。利用他的想法,我們組成一個較爲簡單的網,這個網絡不依賴下一條的選擇。
如下圖:
該算法保留部分可容許的路徑。比如,從源點s到某一頂點i包含了容許邊。算法從部分可容許路徑的末節點(這類節點也稱當前節點)開始執行advance或者retreat步驟。如果從當前節點始發有一些可容許的邊,那麼將會執行算法的advance步驟,並會將這條邊添加到部分可容許的邊中。否則,算法將會執行retreat步驟。
如果部分容許路徑到達了匯點,我們就執行一次增廣。當d(s) >= n是算法結束。另外,Ex(i)的正規表達式:Ex(i) = { (i,j) in E(i): Rij > 0 }。
現在我大概證明一下該算法的運行時間O(n*n*m)。
引理一:算法每步都會保存距離標號。此外,每次重標號都要嚴格地增加一個節點的距離標號。
證明描述:對一些重標號操作和增廣執行歸納法。
引理二:每個節點的距離標號之多增添n次。連續的,重標號操作至多執行n*n次。
證明:引理二是引理一的延伸,如果d(s) >= n,那麼殘留網絡中不在包含增廣路徑。
因爲改進的最短增廣路徑算法產生增廣是沿着最短路徑(和沒有改進的算法一樣),所以增廣的總數都是相同的O(n*m)。執行一次retreat步就重標一個節點,這就是爲什麼retreat steps需要O(n*n)(根據引理二)。執行retreat/relabel步驟的時間是O( n ∑i in V |E(i)| ) = O(nm)。由於一次增廣需要時間O(n),所以總的增廣時間應是O(n*n*m)。advance步驟執行的總時間是增廣時間加上retreat/relabe的時間,也是O(n*n*m)。於是我們得到以下定理:
定理二:改進後的最短增廣路徑算法的運行時間爲O(n*n*m)。
Ahuja and Orlin認爲這是對該算法非常實用的一次改進。因爲當最大流被找到的時候,算法執行了許多無用的重標號操作,解決無效操作更好的辦法就是添加一個終止的條件。我們引入一個(n+1)維的數組numbs,下標從0到n。numbs(k)代表的值是節點的個數,它的參數k等於距離標號。當算法利用BFS計算初始距離標號的同時,初始化這個數組numbs。
當算法從節點x到節點y增加節點的距離標號時,將會從numbs(x)減1,而numbs(y)加1,同時檢查numbs(x)是否等於0。如果等於0,算法終止。
這種方法是一種啓發式,但是它在實際中確實很好用。證明留給讀者。(提示:當節點i有d(i) > x,以及節點j有d(j) < x時產生割,此時就要利用最大流最小割定理。)
第五節 改進後的算法和沒改進算法的比較
本節,我們將在最壞情況下來比較兩種最短增廣路徑算法的運行時間。
最壞情況下,不論是改進的還是沒有改進的算法都會執行O(n*n*n)次。如果m = n*n,Norman Zade開發了一些基於運行時間的例子。利用他的想法,我們組成一個較爲簡單的網,這個網絡不依賴下一條的選擇。
如下圖:
除了源點s和匯點t之外,其他的節點被分爲四個子集:S={s1,...,sk},T={t1,...,tk},U={u1,...,u2p},V={v1,...,v2p}。集合S和集合T包含k個節點,而集合U和集合V包含2p個節點。k和p都是定整數。上圖中用粗體線連接的邊(連接S和T)表示單位容量,用虛線連接的邊表示無窮大容量,其他的邊表示的容量爲k。
首先,最短增廣路徑算法沿着路徑(s,S,T,t)增加流k*k次,此時流的長度等於3,這些路徑的容量都是單位容量。之後,殘留網絡中將包含反向弧(T,S),並且算法將會選擇另外k*k個長度爲7的增廣路徑(s,u1,u2,T,S,v2,v1)。接下來,算法會繼續選擇長度爲11的增廣路徑(s,u1,u2,u3,u4,S,T,v4,v3,v2,v1,t)。如此這般,這般如此,一直執行下去。
這時候,讓我們來計算一下網絡中的一些參數。頂點的個數n = 2*k + 4*p + 2,邊的個數m = k*k + 2*p*k + 2*k +4*p。那麼就很容易的得到增廣的次數a = k*k*(p+1)。
我們在最壞情況下做了五次測試,每次測試的頂點分別爲:100個、148個、202個、250個、298個,並比較了改進後的算法和沒改進的算法的運行時間。從下圖中我們得知,改進的算法更快一些。對於有298個頂點的網絡,改進的算法比沒改進的算法快23倍。通過實踐分析後我們得知:一般情況下,改進後的算法比沒改進的算法快14倍。
如下圖所示:
首先,最短增廣路徑算法沿着路徑(s,S,T,t)增加流k*k次,此時流的長度等於3,這些路徑的容量都是單位容量。之後,殘留網絡中將包含反向弧(T,S),並且算法將會選擇另外k*k個長度爲7的增廣路徑(s,u1,u2,T,S,v2,v1)。接下來,算法會繼續選擇長度爲11的增廣路徑(s,u1,u2,u3,u4,S,T,v4,v3,v2,v1,t)。如此這般,這般如此,一直執行下去。
這時候,讓我們來計算一下網絡中的一些參數。頂點的個數n = 2*k + 4*p + 2,邊的個數m = k*k + 2*p*k + 2*k +4*p。那麼就很容易的得到增廣的次數a = k*k*(p+1)。
我們在最壞情況下做了五次測試,每次測試的頂點分別爲:100個、148個、202個、250個、298個,並比較了改進後的算法和沒改進的算法的運行時間。從下圖中我們得知,改進的算法更快一些。對於有298個頂點的網絡,改進的算法比沒改進的算法快23倍。通過實踐分析後我們得知:一般情況下,改進後的算法比沒改進的算法快14倍。
如下圖所示:
然而,我們的比較結果並不是最可靠的,因爲我們只是用了其中的一種網絡。我們只想證明改進的算法比沒改進的算法的運行速度快,並且快的數量級是線性的。我將在文章的末尾講解一個更爲準確的比較。
第六節 最大容量路徑算法,O(n*n*mlognU) / O(m*m lognU logn) / O(m*m lognU logU)
1972年,Edmonds and Karp發明了另一種找到增廣路徑的方法。在每一步,他們試圖用儘可能最大的數來增加這個流。這個算法的另一個叫法是:Ford-Fulkerson方法梯度修正。這個修正算法代替了用BFS尋找最短路徑,而改爲利用Dijkstra算法來建立最大可能容量的路徑。在增廣之後,算法會在殘留網絡中找到另一條這樣的路徑,並沿着這條路徑增加流,一直重複這幾步直到找到最大流。
第六節 最大容量路徑算法,O(n*n*mlognU) / O(m*m lognU logn) / O(m*m lognU logU)
1972年,Edmonds and Karp發明了另一種找到增廣路徑的方法。在每一步,他們試圖用儘可能最大的數來增加這個流。這個算法的另一個叫法是:Ford-Fulkerson方法梯度修正。這個修正算法代替了用BFS尋找最短路徑,而改爲利用Dijkstra算法來建立最大可能容量的路徑。在增廣之後,算法會在殘留網絡中找到另一條這樣的路徑,並沿着這條路徑增加流,一直重複這幾步直到找到最大流。
毫無疑問,算法在整數容量的條件下是正確的。然而,對於非整數邊容量,經過測試,算法很有可能由於失敗而終止。
我們可以根據某一引理而得到算法的運行時間限制。爲了理解這一證明,我們應該記住在網絡中,任一流的值小於或者等於割的容量,或者閱讀參考文獻[1],參考文獻[2]。我們用c(S,T)來表示割(S,T)的容量。
引理三:讓F表示最大流的值,那麼G包含了容量不小於F/m的增廣路徑。
證明:假設G不包含這樣的路徑。我們構造一個集合E' = { (i,j) in E: Uij ≥ F/m }。令網絡G' = (V, E'),且網絡中沒有從源點s到匯點t的路徑。S是從G中和T = V \ S中獲得的節點的集合。很明顯,(S,T)是一個割並且有c(S,T) >= F。但是割(S,T)只和Uij < F/m的邊相交。所以,很顯然有:
定理三:最大容量路徑算法執行O(mlog(nU))次增廣。
證明:假設算法經過k次增廣後終止。讓f1表示第一次發現增廣路徑的容量,f2表示第二次,依此類推,fk表示第k次增廣路徑的容量。此時,令Fi = f1 + f2 +...+ fi,讓F*表示最大流的值。根據定理三,就可以證明:
我們可以根據某一引理而得到算法的運行時間限制。爲了理解這一證明,我們應該記住在網絡中,任一流的值小於或者等於割的容量,或者閱讀參考文獻[1],參考文獻[2]。我們用c(S,T)來表示割(S,T)的容量。
引理三:讓F表示最大流的值,那麼G包含了容量不小於F/m的增廣路徑。
證明:假設G不包含這樣的路徑。我們構造一個集合E' = { (i,j) in E: Uij ≥ F/m }。令網絡G' = (V, E'),且網絡中沒有從源點s到匯點t的路徑。S是從G中和T = V \ S中獲得的節點的集合。很明顯,(S,T)是一個割並且有c(S,T) >= F。但是割(S,T)只和Uij < F/m的邊相交。所以,很顯然有:
c(S,T) < (F/m)_m = F,
於是,這與事實c(S,T) ≥ F相矛盾。定理三:最大容量路徑算法執行O(mlog(nU))次增廣。
證明:假設算法經過k次增廣後終止。讓f1表示第一次發現增廣路徑的容量,f2表示第二次,依此類推,fk表示第k次增廣路徑的容量。此時,令Fi = f1 + f2 +...+ fi,讓F*表示最大流的值。根據定理三,就可以證明:
fi ≥ (F* - Fi-1) / m.
此時,經過i次連續的增廣 ,我們就可以估算出最大流值和流之間的差異:F* - Fi = F* - Fi-1 - fi ≤ F* - Fi-1 - (F* - Fi-1) / m = (1 - 1 / m) (F* - Fi-1) ≤ ... ≤ (1 - 1 / m)i_F*
我們需找出這樣一個整數i:(1 - 1 / m)i _ F* < 1。這樣就可以證明:
i*logm/(m+1) F* = O(m _ log F*) = O(m_log(nU))
於是這個定理得證。
爲了找到路徑的最大容量,我們用Dijkstra算法,該算法在每次迭代時會帶來額外的開銷。因爲Dijkstras算法的簡單實現的複雜度爲O(n*n),最大容量路徑算法總的運行時間是O(n2mlog(nU))。
對於稀疏網絡,Dijkstra算法利用堆實現的運行時間是O(mlogn),對於最大流則需O(m2 logn log(nU))。看起來這比改進後的Edmonds-Karp算法更好一些,然而,這個估計是極具欺騙性的。
還有另一種的變種方法來找到最大容量路徑,可以利用二分查找來建立最大容量路徑。設找最大容量路徑的區間爲[0,U],如果一些路徑的容量等於U/2,那麼我們繼續在區間[U/2,U]上找這條路徑;否則,我們將在區間[0,U/2-1]上找這條路徑。這種方法需要額外的O(mlogU)開銷,並給出了最大流算法的時間約束O(m*mlog(nU)logU)。不過這種方法在實際中的表現去不怎麼樣。
第七節 容量調整算法,O(m*mlogU)
1985年,Gabow描述了所謂的“位縮放”算法,由於Ahuja and Orlin在本節中描述的是類似容量調整算法。
非正式的,該算法的主要思想是增加沿路徑有足夠大容量的流,而不是沿着最大容量增加。正式的,我們引入一個參數Δ。首先,Δ是個很大的數,例如,令Δ = U。此算法試圖找出一條增廣路徑,且其容量不小於Δ,當在殘留網絡中存在這樣的Δ-路徑時,那麼沿着這條路徑增加流,並重復此過程。
該算法可建立一個最大流或者令Δ/2,並且用新的Δ繼續尋找路徑和增加流量。沿着路徑增加流(容量至少是Δ)的階段被稱爲“Δ縮進階段”或者“Δ階段”。Δ是一個整數值,算法將會執行O(logU)次“Δ階段”。當Δ等於1的時候,容量調整算法和Edmonds-Karp算法將沒有任何區別。
我們可以很容易得到一條容量至少是Δ的路徑---在O(m)時間內(用BFS算法)。開始,我們令Δ的值可以是U或者是Δ的二次方但不能超過U。
引理四:對於每個“Δ-phase”,算法的最壞情況是執行O(m)次增廣。
引理四的證明留給讀者。
應用引理四得到下面的結論:
定理四:容量調整算法的運行時間是O(m2logU)。
請記住,此時當尋找一條增廣路徑時,使用BFS和DFS是沒有任何區別的。但是,在實踐中卻是截然相反的。
第八節 改進的容量調整算法
在上一節,我們介紹了一種運行時間爲O(m*mlogU)的尋找最大流的算法。本節我們將改進此算法,將其運行時間提高至O(n*mlogU)。
現在我們獨立的看看每個“Δ-phase”。回想上一節,每個“Δ-scaling phase”都包含了O(m)次增廣。當描述最短增廣路徑算法的改進型時,我們會將相似的技術應用到“Δ-phase”中。在每個階段,我們通過僅使用的路徑(容量至少等於Δ)來尋找最大流。對改進的最短增廣路徑算法的複雜度分析意味着:如果算法保證執行O(m)次增廣,那麼它將運行O(nm)的時間內,這是因爲增廣的時間從O(n*n*m)減少到O(n*m)以及其他的一些操作,就像前面,需要O(n*m)的時間。這些原因立即對改進的容量調整算法的運行時間形成了O(nmlogU) 的約束。
不幸的是,這種改進在實踐中幾乎對運行時間的降低起不了作用。
第九節 實際的分析和比較
現在,我們來做一些有意思的事情。在這節,我將會以實際應用的觀點來比較前面所有介紹的算法。爲了實現這一目標,在超鏈8的幫助下,我做了一些測試案例,並將它們藉助密度分成三組。第一組測試的網絡滿足:m ≤ n1.4---一些稀疏的網絡;第二組測試的網絡--中等密度網絡滿足:n1.6 ≤ m ≤ n1.7;第三組測試的網絡--幾乎是完全圖(包括完整的非循環網絡)滿足:m ≥ n1.85。
我在前面已經講過所有算法的一些簡單實現。所有的實現都是用鄰接表來表示網絡。
我們先來對第一組做些測試。有564個稀疏網絡,且它們的頂點數都現在2000(如果少於這些,算法運行的太快)。所有的運行時間都是以毫秒爲單位。
於是這個定理得證。
爲了找到路徑的最大容量,我們用Dijkstra算法,該算法在每次迭代時會帶來額外的開銷。因爲Dijkstras算法的簡單實現的複雜度爲O(n*n),最大容量路徑算法總的運行時間是O(n2mlog(nU))。
對於稀疏網絡,Dijkstra算法利用堆實現的運行時間是O(mlogn),對於最大流則需O(m2 logn log(nU))。看起來這比改進後的Edmonds-Karp算法更好一些,然而,這個估計是極具欺騙性的。
還有另一種的變種方法來找到最大容量路徑,可以利用二分查找來建立最大容量路徑。設找最大容量路徑的區間爲[0,U],如果一些路徑的容量等於U/2,那麼我們繼續在區間[U/2,U]上找這條路徑;否則,我們將在區間[0,U/2-1]上找這條路徑。這種方法需要額外的O(mlogU)開銷,並給出了最大流算法的時間約束O(m*mlog(nU)logU)。不過這種方法在實際中的表現去不怎麼樣。
第七節 容量調整算法,O(m*mlogU)
1985年,Gabow描述了所謂的“位縮放”算法,由於Ahuja and Orlin在本節中描述的是類似容量調整算法。
非正式的,該算法的主要思想是增加沿路徑有足夠大容量的流,而不是沿着最大容量增加。正式的,我們引入一個參數Δ。首先,Δ是個很大的數,例如,令Δ = U。此算法試圖找出一條增廣路徑,且其容量不小於Δ,當在殘留網絡中存在這樣的Δ-路徑時,那麼沿着這條路徑增加流,並重復此過程。
該算法可建立一個最大流或者令Δ/2,並且用新的Δ繼續尋找路徑和增加流量。沿着路徑增加流(容量至少是Δ)的階段被稱爲“Δ縮進階段”或者“Δ階段”。Δ是一個整數值,算法將會執行O(logU)次“Δ階段”。當Δ等於1的時候,容量調整算法和Edmonds-Karp算法將沒有任何區別。
引理四:對於每個“Δ-phase”,算法的最壞情況是執行O(m)次增廣。
引理四的證明留給讀者。
應用引理四得到下面的結論:
定理四:容量調整算法的運行時間是O(m2logU)。
請記住,此時當尋找一條增廣路徑時,使用BFS和DFS是沒有任何區別的。但是,在實踐中卻是截然相反的。
第八節 改進的容量調整算法
在上一節,我們介紹了一種運行時間爲O(m*mlogU)的尋找最大流的算法。本節我們將改進此算法,將其運行時間提高至O(n*mlogU)。
現在我們獨立的看看每個“Δ-phase”。回想上一節,每個“Δ-scaling phase”都包含了O(m)次增廣。當描述最短增廣路徑算法的改進型時,我們會將相似的技術應用到“Δ-phase”中。在每個階段,我們通過僅使用的路徑(容量至少等於Δ)來尋找最大流。對改進的最短增廣路徑算法的複雜度分析意味着:如果算法保證執行O(m)次增廣,那麼它將運行O(nm)的時間內,這是因爲增廣的時間從O(n*n*m)減少到O(n*m)以及其他的一些操作,就像前面,需要O(n*m)的時間。這些原因立即對改進的容量調整算法的運行時間形成了O(nmlogU) 的約束。
不幸的是,這種改進在實踐中幾乎對運行時間的降低起不了作用。
第九節 實際的分析和比較
現在,我們來做一些有意思的事情。在這節,我將會以實際應用的觀點來比較前面所有介紹的算法。爲了實現這一目標,在超鏈8的幫助下,我做了一些測試案例,並將它們藉助密度分成三組。第一組測試的網絡滿足:m ≤ n1.4---一些稀疏的網絡;第二組測試的網絡--中等密度網絡滿足:n1.6 ≤ m ≤ n1.7;第三組測試的網絡--幾乎是完全圖(包括完整的非循環網絡)滿足:m ≥ n1.85。
我在前面已經講過所有算法的一些簡單實現。所有的實現都是用鄰接表來表示網絡。
我們先來對第一組做些測試。有564個稀疏網絡,且它們的頂點數都現在2000(如果少於這些,算法運行的太快)。所有的運行時間都是以毫秒爲單位。
從圖表得知,在稀疏網絡中,試圖不用堆實現的Dijkstra的最大容量路徑算法確實是一個嚴重的錯誤。因爲用堆實現的運行速度確實比期望的要快。大約在同一時間執行容量調整算法(使用DFS和BFS),然而改進後的實現時間幾乎是原來的兩倍快。然而令人不解的是,在稀疏網絡中,改進的最短路徑算法被證明是最快的。
現在,我們來看看第二組測試實例。總共做了184次測試,所有網絡的頂點都限制在400個。
在中等密度網絡中,通過二分查找實現的最大容量路徑算法留下了許多不足之處,但是用堆實現仍然比沒有用堆實現的要快。用BFS實現容量調整算法要比用DFS實現快。改進後的調整算法和改進後的最短增廣路徑算法在這次測試中都是很優秀的。
我們很有興趣的想知道這些算法在密集網絡中是怎樣運行的。我們來看看第三組測試:有200個密集網絡,且其頂點限制在400個。
現在,我們看看容量調整算法的BFS和DFS的版本之間的差異。出乎預料的是,用堆實現的Dijkstra的最大容量路徑算法被證明快於沒有用堆實現的算法。
毫無疑問,經過改進後實現的Edmonds-Karp算法贏得了這場遊戲。第二名則是被改進的調整算法拿下,使用BFS的調整容量算法拿下了第三名。
至於最大容量路徑,最好使用一種用堆實現的變種方法;在稀疏網絡中,它能收到很好的效果。而對於其他算法,它們只是適用於理論研究和興趣愛好。
正如你看到的,複雜度爲O(n*mlogU)的算法並不是那麼快的,它甚至比複雜度爲O(n*n*m)的算法還要慢。而我們最常用的卻是複雜度爲O(n*m*m)的算法,雖然此算法有更糟糕的時間範圍,但是它的運行速度比一般算法都要快。
我的建議:始終使用BFS的容量調整路徑算法,因爲它很容易實現。改進的最短增廣路徑算法也是很相當容易實現的,但是你必須要非常小心,正確編寫程序。在比賽中,它是很容易錯過的一個bug。
在結束本文之前,我給出了改進的雖短增廣路徑算法的完整實現。我用鄰接矩陣表示這個網絡,這樣能更好的理解算法。在實際分析中我們用的是不一樣的實現,鄰接矩陣比鄰接表實現起來相對慢一些。不過,最終還是由讀者選擇最適合自己的數據結構。
[1] Ravindra K. Ahuja, Thomas L. Magnanti, and James B. Orlin. Network Flows: Theory, Algorithms, and Applications.
[2] Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest. Introduction to Algorithms.
[3] Ford, L. R., and D. R. Fulkerson. Maximal flow through a network.
[4] Norman Zadeh. Theoretical Efficiency of the Edmonds-Karp Algorithm for Computing Maximal Flows.
[5] _efer_. Algorithm Tutorial: MaximumFlow.
[6] gladius. Algorithm Tutorial: Introduction to graphs and their data structures: Section 1.
[7] gladius. Algorithm Tutorial: Introduction to graphs and their data structures: Section 3.
[8] http://elib.zib.de/pub/mp-testdata/generators/index.html -- A number of generators for network flow problems.
現在,我們來看看第二組測試實例。總共做了184次測試,所有網絡的頂點都限制在400個。
我們很有興趣的想知道這些算法在密集網絡中是怎樣運行的。我們來看看第三組測試:有200個密集網絡,且其頂點限制在400個。
毫無疑問,經過改進後實現的Edmonds-Karp算法贏得了這場遊戲。第二名則是被改進的調整算法拿下,使用BFS的調整容量算法拿下了第三名。
至於最大容量路徑,最好使用一種用堆實現的變種方法;在稀疏網絡中,它能收到很好的效果。而對於其他算法,它們只是適用於理論研究和興趣愛好。
正如你看到的,複雜度爲O(n*mlogU)的算法並不是那麼快的,它甚至比複雜度爲O(n*n*m)的算法還要慢。而我們最常用的卻是複雜度爲O(n*m*m)的算法,雖然此算法有更糟糕的時間範圍,但是它的運行速度比一般算法都要快。
我的建議:始終使用BFS的容量調整路徑算法,因爲它很容易實現。改進的最短增廣路徑算法也是很相當容易實現的,但是你必須要非常小心,正確編寫程序。在比賽中,它是很容易錯過的一個bug。
在結束本文之前,我給出了改進的雖短增廣路徑算法的完整實現。我用鄰接矩陣表示這個網絡,這樣能更好的理解算法。在實際分析中我們用的是不一樣的實現,鄰接矩陣比鄰接表實現起來相對慢一些。不過,最終還是由讀者選擇最適合自己的數據結構。
/******improved shortest augmenting path algorithm******/ #include <stdio.h> #define N 2007 // Number of nodes #define oo 1000000000 // Infinity // Nodes, Arcs, the source node and the sink node int n, m, source, sink; // Matrixes for maintaining // Graph and Flow int G[N][N], F[N][N]; int pi[N]; // predecessor list int CurrentNode[N]; // Current edge for each node int queue[N]; // Queue for reverse BFS int d[N]; // Distance function int numbs[N]; // numbs[k] is the number of nodes i with d[i]==k // Reverse breadth-first search // to establish distance function d int rev_BFS() { int i, j, head(0), tail(0); // Initially, all d[i]=n for(i = 1; i <= n; i++) numbs[ d[i] = n ] ++; // Start from the sink numbs[n]--; d[sink] = 0; numbs[0]++; queue[ ++tail ] = sink; // While queue is not empty while( head != tail ) { i = queue[++head]; // Get the next node // Check all adjacent nodes for(j = 1; j <= n; j++) { // If it was reached before or there is no edge // then continue if(d[j] < n || G[j][i] == 0) continue; // j is reached first time // put it into queue queue[ ++tail ] = j; // Update distance function numbs[n]--; d[j] = d[i] + 1; numbs[d[j]]++; } } return 0; } // Augmenting the flow using predecessor list pi[] int Augment() { int i, j, tmp, width(oo); // Find the capacity of the path for(i = sink, j = pi[i]; i != source; i = j, j = pi[j]) { tmp = G[j][i]; if(tmp < width) width = tmp; } // Augmentation itself for(i = sink, j = pi[i]; i != source; i = j, j = pi[j]) { G[j][i] -= width; F[j][i] += width; G[i][j] += width; F[i][j] -= width; } return width; } // Relabel and backtrack int Retreat(int &i) { int tmp; int j, mind(n-1); // Check all adjacent edges // to find nearest for(j=1; j <= n; j++) // If there is an arc // and j is "nearer" if(G[i][j] > 0 && d[j] < mind) mind = d[j]; tmp = d[i]; // Save previous distance // Relabel procedure itself numbs[d[i]]--; d[i] = 1 + mind; numbs[d[i]]++; // Backtrack, if possible (i is not a local variable! ) if( i != source ) i = pi[i]; // If numbs[ tmp ] is zero, algorithm will stop return numbs[ tmp ]; } // Main procedure int find_max_flow() { int flow(0), i, j; rev_BFS(); // Establish exact distance function // For each node current arc is the first arc for(i=1; i<=n; i++) CurrentNode[i] = 1; // Begin searching from the source i = source; // The main cycle (while the source is not "far" from the sink) for( ; d[source] < n ; ) { // Start searching an admissible arc from the current arc for(j = CurrentNode[i]; j <= n; j++) // If the arc exists in the residual network // and if it is an admissible if( G[i][j] > 0 && d[i] == d[j] + 1 ) // Then finish searhing break; // If the admissible arc is found if( j <= n ) { CurrentNode[i] = j; // Mark the arc as "current" pi[j] = i; // j is reachable from i i = j; // Go forward // If we found an augmenting path if( i == sink ) { flow += Augment(); // Augment the flow i = source; // Begin from the source again } } // If no an admissible arc found else { CurrentNode[i] = 1; // Current arc is the first arc again // If numbs[ d[i] ] == 0 then the flow is the maximal if( Retreat(i) == 0 ) break; } } // End of the main cycle // We return flow value return flow; } // The main function // Graph is represented in input as triples <from, to, capacity> // No comments here int main() { int i, p, q, r; scanf("%d %d %d %d", &n, &m, &source, &sink); for(i = 0; i < m; i++) { scanf("%d %d %d", &p, &q, &r); G[p][q] += r; } printf("%d", find_max_flow()); return 0; }參考文獻:
[1] Ravindra K. Ahuja, Thomas L. Magnanti, and James B. Orlin. Network Flows: Theory, Algorithms, and Applications.
[2] Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest. Introduction to Algorithms.
[3] Ford, L. R., and D. R. Fulkerson. Maximal flow through a network.
[4] Norman Zadeh. Theoretical Efficiency of the Edmonds-Karp Algorithm for Computing Maximal Flows.
[5] _efer_. Algorithm Tutorial: MaximumFlow.
[6] gladius. Algorithm Tutorial: Introduction to graphs and their data structures: Section 1.
[7] gladius. Algorithm Tutorial: Introduction to graphs and their data structures: Section 3.
[8] http://elib.zib.de/pub/mp-testdata/generators/index.html -- A number of generators for network flow problems.