求最大流有一種經典的演算法,就是每次找增廣路時用BFS找,保證找到的增廣路是弧數最少的,也就是所謂的Edmonds-Karp演算法。可以證明的是在使用最短路增廣時增廣過程不超過V*E次,每次BFS的時間都是O(E),所以Edmonds-Karp的時間複雜度就是O(V*E^2)。
如果能讓每次尋找增廣路時的時間複雜度降下來,那麼就能提高演算法效率了,使用距離標號的最短增廣路演算法就是這樣的。所謂距離標號,就是某個點到匯點的最少的弧的數量(另外一種距離標號是從源點到該點的最少的弧的數量,本質上沒什麼區別)。設點i的標號為D[i],那麼如果將滿足D[i]=D[j]+1的弧(i,j)叫做允許弧,且增廣時只走允許弧,那麼就可以達到“怎麼走都是最短路”的效果。每個點的初始標號可以在一開始用一次從匯點沿所有反向邊的BFS求出,問題就是如何在增廣過程中維護這個距離標號。
維護距離標號的方法是這樣的:當找增廣路過程中發現某點出發沒有允許弧時,將這個點的距離標號設為由它出發的所有弧的終點的距離標號的最小值加一。這種維護距離標號的方法的正確性我就不證了。
由於距離標號的存在,由於“怎麼走都是最短路”,所以就可以採用DFS找增廣路,用一個棧儲存當前路徑的弧即可。當某個點的距離標號被改變時,棧中指向它的那條弧肯定已經不是允許弧了,所以就讓它出棧,並繼續用棧頂的弧的端點增廣。為了使每次找增廣路的時間變成均攤O(V),還有一個重要的最佳化是對於每個點儲存“當前弧”:初始時當前弧是鄰接表的第一條弧;在鄰接表中尋找時從當前弧開始尋找,找到了一條允許弧,就把這條弧設為當前弧;改變距離標號時,把當前弧重新設為鄰接表的第一條弧,還有一種在常數上有所最佳化的寫法是改變距離標號時把當前弧設為那條提供了最小標號的弧。當前弧的寫法之所以正確就在於任何時候我們都能保證在鄰接表中當前弧的前面肯定不存在允許弧。
還有一個常數最佳化是在每次找到路徑並增廣完畢之後不要將路徑中所有的頂點退棧,而是只將瓶頸邊以及之後的邊退棧,這是借鑒了Dinic演算法的思想。注意任何時候待增廣的“當前點”都應該是棧頂的點的終點。這的確只是一個常數最佳化,由於當前邊結構的存在,我們肯定可以在O(n)的時間內複原路徑中瓶頸邊之前的所有邊。
我程式做了很多最佳化(甚至包括動態記憶體分配的時候使用placement new進行最佳化),最終能做到比wxs寫的dinic快了大約一倍,但程式只有一百二十多行,我對這個結果很滿意。距離標號增廣的確是一個非常優秀非常實用的演算法。
樣本程式(USACO Training Ditch):
ditch.cpp
June 4, 2007 · Filed under 程式園