7. 網絡流算法--Ford-Fulkerson方法及其多種實現


網絡流

在上一章中我們討論的主題是圖中頂點之間的最短路徑,例如公路地圖上兩地點之間的最短路徑,所以我們將公路地圖抽象為有向帶權圖。本章我們將對基於有向帶權圖的模型做進一步擴展。

很多系統中涉及流量問題,例如公路系統中車流量,網絡中的數據信息流,供油管道的油流量等。我們可以將有向圖進一步理解為“流網絡”(flow network),並利用這樣的抽象模型求解有關流量的問題。

 

圖 電路原理圖可抽象為網絡流

流網絡中每條有向邊可以認為是傳輸物質的管道,每個管道有固定的容量,可以看作是物質能夠流經該管道的最大速度。頂點是管道之間的交叉連接點,除了匯點之外,物質只流經這些點,不會再頂點滯留或消耗。也就是說,物質進入某頂點的速度必須等於離開該頂點的速度。這一特性被稱為“流守恆”(flow conservation)。例如中的電路原理圖,根據基爾霍夫電流定律,在每個交叉連接點出,流進的電流等於流出的電流。電流的定義為單位時間內通過導線某一截面的電荷量,即為電荷的流動速度。所以,用流守恆的觀點可以理解為:電荷量流進某交叉頂點的速度等於離開該頂點的速度。

在本章我們將討論最大流問題,這是流網絡中最簡單的問題:在不違背容量限制的條件下,求解把物質從源點傳輸到匯點的最大速率。本章主要介紹流網絡和流的基本概念和性質,並提供流網絡的數據結構描述和實現,以及一種解決最大流的經典方法及其算法實現,即Ford-Fulkerson方法。

.1 流網絡

網絡流G=(v, E)是一個有向圖,其中每條邊(u, v)均有一個非負的容量值,記為c(u, v) ≧ 0。如果(u, v)  E則可以規定c(u, v) = 0。網絡流中有兩個特殊的頂點,即源點s和匯點t。

與網絡流相關的一個概念是流。設G是一個流網絡,其容量為c。設s為網絡的源點,t為匯點,那么G的流是一個函數f:V×V R,滿足一下性質:

容量限制:對所有頂點對u,v∈V,滿足f(u, v) ≦ c(u, v);

反對稱性:對所有頂點對u,v∈V,滿足f(u, v) = - f(v, u);

流守恆性:對所有頂點對u∈V-{s, t},滿足ΣvVf(u,v)=0。

f(u, v)稱為從頂點u到頂點v的流,流的值定義為:

|f| =ΣvVf(s,v),

即從源點s出發的總的流。

在最大流問題中,我們需要求解源點s到匯點t之間的最大流f(s, t),同時我們還希望了解達到該值的流。對於一個指定的源點s和指定匯點t的網,我們稱之為st-網。

如圖所示為一個流網絡,其中頂點之間的邊的粗細對應着邊的容量大小。

 

 

 

 

圖 有向圖表示網絡流

下面以為例,在流的三個性質條件下嘗試性地尋找圖中的最大流,如圖(a~c)。

 

 

 

 

 

從上圖(a~c)中可以發現,流網絡從源點s流出的量依次為2,3,5,而流入匯點t的流量也2,3,5。事實上,任何流從s流出量總應該等於到匯點t的流入量,下面對這一命題做簡單證明。

 

 

 

構造:如圖(a),對原流網絡做擴展,增加頂點s’一條邊(s, s),邊的流和容量都與從s流出的流的值相等;增加頂點t和一條邊(t, t),邊的流和容量都與到t的流的值相等。

我們要證明s的流出量等於t的流入量,只要證明對任意頂點集合,流出量等於流入量即可。采用歸納證明。

證明:對於單個頂點構成的頂點集合,其流出量必然等於流出量;假設,對於一給定的頂點集合A此屬性成立,則需要驗證增加一個頂點v后得到的新的集合A=A∪{v}也滿足此屬性。

如圖,對集合A,從v流入的流記為f3,其它的流入量合計為f1;流出到v的流記為f4,其它的流出流量合計為f6。注意,這里的流都指的是流的值,都是非負的。

A的流入量為fin(A) = f1 + f3,流出量為fout(A) = f2 + f4;根據假設可以得出關系:

f1 + f3 = f2 + f4;

對頂點v,根據流的第二條性質,得出關系:

f6 + f3 = f5 + f4。

根據上面兩個等式,可以得到關系:

f1  f6 = f2  f5,

即:

f1 + f5 = f2 + f6。

A的流入量fin(A) = f1 + f5,流出量fout(A) = f2 + f6,所以集合A滿足屬性。

將這個屬性應用於擴展前的原始流網絡中的所有頂點,可以得出邊(s, s)上的流等於邊(t, t)上的流,也就是從s流出量等於到匯點t的流入量。

.2 Ford-Fulkerson方法

本節開始討論解決最大流問題的Ford-Fulkerson方法,該方法也稱作“擴充路徑方法”,該方法是大量算法的基礎,有多種實現方法。在以后章節中我們將介紹並分析一種特定的算法。

Ford-Fulkerson算法是一種迭代算法,首先對圖中所有頂點對的流大小清零,此時的網絡流大小也為0。在每次迭代中,通過尋找一條“增廣路徑”(augument path)來增加流的值。增廣路徑可以看作是源點s到匯點t的一條路徑,並且沿着這條路徑可以增加更多的流。迭代直至無法再找到增廣路徑位置,此時必然從源點到匯點的所有路徑中都至少有一條邊的滿邊(即邊的流的大小等於邊的容量大小)。

這里提及一個新的概念,即“增廣路徑”。下面我們將進一步引入“殘留網絡”(residual network)來討論增廣路徑的尋找算法,並引入“最大流最小割”(Max-Flow Min Cut)定理來證明Ford-Fulkerson算法的正確性。

.2.1 殘留網

給定一個流網絡G和一個流,流的殘留網Gf擁有與原網相同的頂點。原流網絡中每條邊將對應殘留網中一條或者兩條邊,對於原流網絡中的任意邊(u, v),流量為f(u, v),容量為c(u, v)

如果f(u, v) > 0,則在殘留網中包含一條容量為f(u, v)的邊(v, u);

如果f(u, v) < c(u, v),則在殘留網中包含一條容量為c(u, v) - f(u, v)的邊(u, v)。

殘留網允許我們使用任何廣義圖搜索算法來找一條增廣路徑,因為殘留網中從源點s到匯點t的路徑都直接對應着一條增廣路徑。以為例,具體分析增廣路徑及其相應殘留網,如圖(a~d)。

 

 

 

(a)原始圖流網絡,每條邊上的流都為0。因為f(u, v) = 0 < c(u, v)則在殘留網中包含容量為c(u, v)的邊(u, v)所以此時殘留圖中頂點與原始流網絡相同,邊也與原始流網絡相同,並且邊的容量與原始流網絡相同。

 

    在殘留網中可以找到一條增廣路徑<v0, v1, v3, v5>,每條邊的流為2,此原始流網絡和殘留網中相應的邊會有所變化,如下圖。

 

 

 

(b)在操作(a)之后,路徑<v0, v1, v3, v5>上有了大小為2的流,此時需要對殘留圖中相應的邊做調整:

f(0, 1) > 0,在殘留圖中有容量為2的邊(1, 0);

c(1, 3) > f(1, 3) > 0,在殘留圖中有容量為1的邊(1, 3)和容量為2的邊(3, 1);

f(3, 5) > 0,在殘留圖中有容量為2的邊(5, 3).

在殘留網中可以找到一條增廣路徑<v0, v2, v4, v5>,每條邊的流為1,此原始流網絡和殘留網會有所變化,如下圖。

 

 

 

(c)在操作(b)之后,路徑<v0, v2, v4, v5>上有了大小為1的流,此時需要對殘留圖中相應的邊做調整:

c(0, 2) > f(0, 2) > 0,在殘留圖中有容量為2的邊(0, 2)和容量為1的邊(2, 0);

f(2, 4) > 0,在殘留圖中有容量為1的邊(4, 2);

c(4, 5) > f(4, 5) > 0,在殘留圖中有容量為2的邊(4, 5)和容量為1的邊(5, 4).

進一步在殘留網中可以找到一條增廣路徑<v0, v2, v3, v1, v4, v5>,每條邊的流為1,此原始流網絡和殘留網會有所變化,如下圖。

 

 

 

(d)在操作(c)之后,路徑<v0, v2, v3, v1, v4, v5>上有了大小為1的流,此時需要對殘留圖中相應的邊做調整:

c(0, 2) > f(0, 2) > 0,在殘留圖中有容量為1的邊(0, 2)和容量為2的邊(2, 0);

f(2, 3) > 0,在殘留圖中有容量為1的邊(3, 2);

c(3, 1) > f(3, 1) > 0,在殘留圖中有容量為1的邊(3, 1)和容量為2的邊(1, 3);

f(1, 4) > 0,在殘留圖中有容量為1的邊(4, 1);

c(4, 5) > f(4, 5) > 0,在殘留圖中有容量為1的邊(4, 5)和容量為2的邊(5, 4);

此時殘留圖中無法再找到頂點0到頂點5的路徑,則迭代結束,我們認為圖d中即為尋找到的最大流。

 

2 最大流最小割

我們剛剛討論了基於殘留網的增廣路徑的尋找方法,這里我們將證明Ford-Fulkerson算法迭代停止時得到的流是最大流,即一個流是最大流,當且僅當殘留網中不包含增廣路徑。該命題的證明需要借助於流網絡中的一個重要定理,即最大流最小割定理。

流網絡G=(V, E)的割(S, T)將V分為S和T=V-S兩個部分,使得源點s∈S,匯點t∈T。如果f是一個流,則穿過割(S, T)的流用f(S, T) = ΣuSΣv∈T f(u, v)表示,割(S, T)的容量用C(S, T) = ΣuSΣv∈T c(u, v)如圖流網絡的一個割為({s, v1, v2},{v3, v4, t})

 

 

 

 

 (a)流網絡每條邊上是容量大小      (b)流網絡的一個割,邊上是流的大小

通過該割的流量為:

f(S, T) = Σu{s, v1, v2}Σv∈{v3, v4, t} f(u, v)

  = f(v1, v3) + f(v2, v3) + f(v2, v4)

  = 12 + (-4) + 11 = 19

容量為:

C(S, T) = Σu{s, v1, v2}Σv∈{v3, v4, t} c(u, v)

  = c(v1, v3) + c(v2, v4)

  = 12 + 14 = 26

其中割的流可能是正數也可能是負數,而容量一定是非負的。在流網絡中,每個割的流都是相同的,其值等於流網絡的流的值;並且每個割的流都不大於割的容量。

如圖s’為擴展的頂點,其中邊(s, s)的流和容量都等於頂點s的流出量,記為f1。虛線將流網絡分為兩個集合S和T,形成割(S, T)。從S流出的流量為f2,流入S的流量為f3。第一節中我們證明了流網絡中的頂點集合的流入量等於流出量,所以f1 + f2 = f3。

即f1 = f3  f2,其中f1等於流網絡的流的值,f3-f2為割(S, T)的流量,所以,割的流等於流網絡的流的值。

 

 

 

 

在上圖中,計算割(S, T)的流量時f3的提供正的流量值,而f2提供的是負的流量值,並且在計算割的容量時只有提供流量f3的邊的容量參與相加,根據流的第一條性質,f3的值不會大於割的容量,所以:

f(S, T) = f3  f2 ≦ f3 ≦ C(S, T)。

由於流網絡中所有割的流都相等並且等於網絡的流,所有網絡的任何流的值都不大於任何一個割的容量。

根據上面對流網絡的中割的概念的介紹,下面引入最大流最小割定理,並利用該定理說明Ford-Fulkerson算法的正確性。

最大流最小割定理:一個網中所有流中的最大值等於所有割中的最小容量。並且可以證明一下三個條件等價:

f是流網絡G的一個最大流;

殘留網Gf不包含增廣路徑;

G的某個割(S, T),滿足f(S, T) = c(S, T).

證明: 

1.(反證法)假設f是G的最大流,但是Gf中包含增廣路徑p。顯然此時沿着增廣路徑可以繼續增大網絡的流,則f不是G的最大流,與條件矛盾;

2.假設Gf中不包含增廣路徑,即Gf中不包含從s到t的路徑。定義:

S = {v∈V:Gf中包含s到v的路徑},

令T = V  S,由於Gf中不存在從s到t的路徑,則tS,所以得到G的一個割(S, T)。對每對頂點u∈S,v∈T,必須滿足f(u, v) = c(u, v),否則邊(u, v)就會存在於Gf的邊集合中,那么v就應當屬於S(而事實上是v∈T)。所以,f(S, T) = c(S, T);

3.我們已經證明,網絡的任何流的值都不大於任何一個割的容量,如果G的某個割(S, T),滿足f(S, T) = c(S, T),則說明割(S, T)的流達到了網絡流的上確界,它必然是最大流。

Ford-Fulkerson算法的迭代終止條件是殘留網中不包含增廣路徑,根據上面的等價條件,此時得到的流就是網絡的最大流。

.3 Ford-Fulkerson方法的實現

在前一節,我們討論了Ford-Fulkerson方法中所應用到的幾個概念以及保證該方法正確性的重要屬性。本節將討論Ford-Fulkerson方法的具體實現,包括殘留網的更新和增廣路徑的獲取。

增廣路徑事實上是殘留網中從源點s到匯點t的路徑,可以利用圖算法中的任意一種被算法來獲取這條路徑,例如BFS,DFS等。其中基於BFS的算法通常稱為Edmonds-Karp算法,該算法是“最短”擴充路徑,這里的“最短”由路徑上的邊的數量來度量,而不是流量或者容量。

這里所選的路徑尋找方法會直接影響算法的運行時間,例如,對采用DFS的方法搜索殘留網中的增廣路徑。圖(b)中是第一次搜索得到的增廣路徑為<s, v1, v2, t>,路徑的流大小為1;圖(c)和(d)中搜索得到的增廣路徑的流大小也是1。可以發現,在這個例子中,采用DFS算法將需要2000000次搜索才能得到最大流。

 

 

 

如果換一種方法對殘留網中的進行遍歷將會很快求得流網絡的最大流。如圖第一次在頂點1搜索下一條邊時,不是選擇邊(1, 2)而是選擇容量更大的邊(1, t);第二次在頂點2處搜索下一條邊時,選擇邊(2, t)。這樣只要兩次遍歷即可求解最大流。可見,在殘留網中搜索增廣路徑的算法直接影響Ford-Fulkerson方法實現的效率。

 

 

 

3.1 流網絡數據結構

.3.1.1 流網絡邊的數據結構

流網絡數據結構與圖數據結構比較相似,首先也需要設計流網絡的邊的數據結構。這里我們只討論基於連接表的流網絡數據結構的實現。在圖數據結構中邊包含了源點、終點以及邊所在其對應鏈表中的節點的指針。

流網絡邊種同樣包含上述三個成員,但還包括其它針對流網絡算法的成員函數,其實現如下:

 

 

public  class NetworkEdge {
    
     // 私有成員變量
    
// 邊的源頂點和終節點
     private  int vert1, vert2;
     // 單鏈表節點,
     private SingleNode itself;
     // 構造函數
     public NetworkEdge( int _v1,  int _v2, SingleNode _it){
        vert1 = _v1;
        vert2 = _v2;
        itself = _it;
    }
    
     public  int get_v1() {
         return vert1;
    }
    
     public  int get_v2() {
         return vert2;
    }
    
     public SingleNode get_lk(){
         return itself;
    }
     //  判斷v是否是源點
     public  boolean from( int v){    
         return v == get_v1();
    }
    
     //  返回邊的v頂點的另一頂點
     public  int other( int v){
         return from(v)?vert2:vert1;
    }
}

 

其中函數from判斷給定頂點v是否是這條邊的源點,如果是則返回true,否則返回false;給定頂點v,函數other返回這條邊的另一頂點。

.3.1.2 流網絡數據結構

流網絡數據結構的連接表法的實現與圖數據結構類似。需要定義一個鏈表來存放與給定頂點相鄰的頂點,以及這兩個頂點形成的邊的信息,在流網絡中,邊的信息包括邊的容量和邊的流量。所以鏈表的節點設計為:

 

 

public  class NetworkLLinkNode  implements Comparable{
     // 私有成員,終點、權重、流
     private  int des, cap, flow;
     // 構造函數
     public NetworkLLinkNode( int _des,  int _wt,  int _flow){
        des = _des;
        cap = _wt;
        flow  = _flow;
    }
    
     //  設置終點 
     public  void set_des( int _d){
        des = _d;
    }
    
     //  設置權重
     public  void set_wt( int _wt){
        cap = _wt;
    }
    
     //  設置流
     public  void set_flow( int f){
        flow = f;
    }
    
     //  獲取終點
     public  int get_des(){
         return des;
    }
    
     //  獲取權重
     public  int get_wt(){
         return cap;
    }
    
     //  獲取流
     public  int get_flow(){
         return flow;
    }
    
     //  比較兩個兩個頂點的權重
     public  int compareTo(Object arg0) {
         int _wt = ((NetworkLLinkNode)(arg0)).get_wt();
         if(cap > _wt)  return 1;
         else  if(cap < _wt)  return -1;
         else  return 0;
    }
}

 

其中成員變量包括邊的終點、容量和流,函數compareTo比較相同源點的兩條邊的容量。

基於流網絡邊和鏈表節點數據結構,流網絡數據結構的實現如下:

 

 

public  class Network {
     //     私有成員變量
    
// 頂點鏈表數組,數組的每個元素對應於
    
// 與頂點相連的所有頂點形成的鏈表
     private NetworkNodeLList[] vertexList;
     // 邊的個數和頂點的個數
     private  int num_Edge, num_Vertex;
     // 節點標記數組
     private  int[] mark;
     public Network( int n){
        vertexList =  new NetworkNodeLList[n];
         for( int i = 0; i < n; i++){
            vertexList[i] =  new NetworkNodeLList();
        }
        num_Edge = 0;
        num_Vertex = n;
        mark =  new  int[n];
    }
    
     public  int get_nv() {
         return num_Vertex;
    }
    
     public  int get_ne() {
         return num_Edge;
    }
    
     public NetworkEdge firstEdge( int v) {
        vertexList[v].goFirst();
         if(vertexList[v].getCurrVal() ==  nullreturn  null;
         return  new NetworkEdge(v, 
                ((NetworkLLinkNode)(vertexList[v].getCurrVal()
                        .getElem())).get_des(), 
                vertexList[v].currNode());
    }
    
     public NetworkEdge nextEdge(NetworkEdge w) {
         if(w ==  nullreturn  null;
         int v = w.get_v1();
        vertexList[v].setCurr(w.get_lk());
        vertexList[v].next();
         if(vertexList[v].getCurrVal() ==  nullreturn  null;
         return  new NetworkEdge(v, 
                ((NetworkLLinkNode)(vertexList[v].getCurrVal()
                        .getElem())).get_des(), 
                vertexList[v].currNode());
    }
    
     public  boolean isEdge(NetworkEdge w) {
         if(w ==  nullreturn  false;
         int v = w.get_v1();
        vertexList[v].setCurr(w.get_lk());
         if(!vertexList[v].inList())  return  false;
         return ((NetworkLLinkNode)(vertexList[v].getCurrVal()
                .getElem())).get_des() == w.get_v2();
    }
    
     public  boolean isEdge( int i,  int j) {
         for(vertexList[i].goFirst();
            vertexList[i].getCurrVal() !=  null &&
                ((NetworkLLinkNode)(vertexList[i].getCurrVal()
                        .getElem())).get_des() < j;
            vertexList[i].next());
         return vertexList[i].getCurrVal() !=  null && 
                ((NetworkLLinkNode)(vertexList[i].getCurrVal()
                        .getElem())).get_des() == j;
    }
    
    
     public  int edge_v1(NetworkEdge w) {
         if(w ==  nullreturn -1;
         return w.get_v1();
    }
    
     public  int edge_v2(NetworkEdge w) {
         if(w ==  nullreturn -1;
         return w.get_v2();
    }
    
     public  void setEdgeC( int i,  int j,  int wt) {
         if(i < 0 || j < 0)  return;
        NetworkLLinkNode gln =  new NetworkLLinkNode(j, wt, 0);
         if(isEdge(i, j)){ 
            vertexList[i].setCurrVal(
                     new ElemItem<NetworkLLinkNode>(gln));}
         else{
            vertexList[i].insert(
                     new ElemItem<NetworkLLinkNode>(gln));
            num_Edge++;
        }
    }
    
     public  void setEdgeC(NetworkEdge w,  int wt) {
         if(w !=  null)
            setEdgeC(w.get_v1(), w.get_v2(), wt);
        
    }
    
     public  int getEdgeC( int i,  int j) {
         if(isEdge(i, j))
             return ((NetworkLLinkNode)(vertexList[i].
                    getCurrVal().getElem())).get_wt();
         else  return Integer.MAX_VALUE;
    }
    
     public  int getEdgeC(NetworkEdge w) {
         if(w !=  null
             return getEdgeC(w.get_v1(), w.get_v2());
         else 
             return Integer.MAX_VALUE;
    }
    
     /**
     * 新添加的函數,獲取i為始點,j為終點的邊
     
*/
     public NetworkEdge getNetworkEdge( int i,  int j){
         if(isEdge(i, j))
             return  new NetworkEdge(i, j, 
                    vertexList[i].currNode());
         else  return  null;
    }
    
     public  void setEdgeFlow( int i,  int j,  int flow) {
         if(i < 0 || j < 0)  return;
         int wt = getEdgeC(i, j);
        NetworkLLinkNode gln =  new NetworkLLinkNode(j, wt, flow);
         if(isEdge(i, j)){ 
            vertexList[i].setCurrVal(
                     new ElemItem<NetworkLLinkNode>(gln));}
         else{
            vertexList[i].insert(
                     new ElemItem<NetworkLLinkNode>(gln));
            num_Edge++;
        }
    }
    
     public  void setEdgeFlow(NetworkEdge w,  int flow) {
         if(w !=  null)
            setEdgeFlow(w.get_v1(), w.get_v2(), flow);
    }
    
     public  int getEdgeFlow( int i,  int j) {
         if(isEdge(i, j))
             return ((NetworkLLinkNode)(vertexList[i].getCurrVal()
                    .getElem())).get_flow();
         else  return Integer.MAX_VALUE;
    }
    
     public  int getEdgeFlow(NetworkEdge w) {
         if(w !=  nullreturn getEdgeFlow(w.get_v1(), w.get_v2());
         else  return Integer.MAX_VALUE;
    }
    
     public  void addflowRto(NetworkEdge w,  int v,  int d){
         int pflow = (w.get_v1() == v)?(-1 * d):d;
        pflow += getEdgeFlow(w);
        setEdgeFlow(w, pflow);
        
    }
    
     public  void delEdge( int i,  int j) {
         if(isEdge(i, j)){
            vertexList[i].remove();
            num_Edge--;
        }
    }
    
     public  void delEdge(NetworkEdge w) {
         if(w !=  null)
            delEdge(w.get_v1(), w.get_v2());
    }
    
     public  void setMark( int v,  int val) {
         if(v >= 0 && v < num_Vertex) mark[v] = val;
        
    }
    
     public  int getMark( int v) {
         if(v >= 0 && v < num_Vertex)  return mark[v];
         else  return -1;
    }
    
     int getEdgeCap(NetworkEdge e) { return  this.getEdgeC(e); }
    
     //  如果v是e的起點,則返回e的流(f);若v是e的終點,則返回e的容量-e的流(c-f)
     int capRto(NetworkEdge e,  int v) {
         return e.from(v)?getEdgeFlow(e):(getEdgeC(e) - getEdgeFlow(e));
    }
    
}

 

流網絡與圖數據結構的差別包括以下幾點:

setEdgeC函數設置網絡中邊的容量,對應圖結構中設置圖的邊的權重。一般而言,網絡中邊的容量通常很少改變,所以該函數通常只在創建流網絡時被調用;

setEdgeFlow函數設置網絡邊上的流,其實現與setEdgeC很類似,在最大流算法中網絡邊的流的大小是不斷更新的,該函數便實現邊上流的更新。

addflowRto函數對給定邊上的流進行更新,給定邊w,頂點v和流量d,如果v是邊w的源點則將邊上的流增加d,否則減去d;

capRto函數返回給定邊上的流量,在講解流網絡相關概念時,我們提到,對給定的邊(u, v),f(u, v) = -f(v, u);該函數形參為給定的邊e和頂點v,如果v是e的源點,則返回邊e上的流,否則返回流的相反數。

.3.2 優先隊列搜索

本節將討論有向帶權圖的一個新的搜索算法,稱為基於優先隊列的圖搜索算法。首先將介紹基於下標堆得優先隊列數據結構,並在下文介紹利用該數據結構對Ford-Fulkerson算法的改進。

.3.2.1 基於下標堆的優先隊列

本節首先介紹基於下標對的優先隊列數據結構。假設要在優先隊列中處理的記錄在一個已存在的數組中,可以讓優先隊列例程通過數組下標來引用數據項。這樣隊列中只需要數組的下標,所有對優先隊列的操作都是對數組下標的操作。這里之所以要討論這種優先隊列,主要是因為在圖數據結構中我們使用頂點的標號來訪問頂點,我們可以將頂點的標號作為優先隊列中的元素項,通過這種映射方式可以更高效地利用優先隊列處理有向帶權圖。

這里基於下標的優先隊列與前面章節中討論的優先隊列的基本操作類似,讀者可以溫習一下前面關於堆和優先隊列的內容。基於下標的優先隊列的實現如下:

 

 

public  class intPQi {
     //  存放元素內容的數組
     private ElemItem[] a;
     //  序號的優先隊列,元素的優先級
     private  int[] pq, qp;
     //  元素總數
     private  int N;
     //  類型,-1表示最大堆;1表示最小堆。
     private  int type;
    
     /**
     * 構造函數
     * 
@param  items    元素項數組
     
*/
     public intPQi(ElemItem[] items,  int type){
        a = items; N = 0;
        pq =  new  int[a.length + 1];
        qp =  new  int[a.length + 1];
         this.type = type;
    }
    
     /**
     * 比較a[i]和a[j]
     * 
@param  i, j    第i, j個元素
     * 
@return  type = -1時,
     * 如果a[i]小於a[j]返回true,否則false
     
*/
     private  boolean less( int i,  int j){
         int c = a[pq[i]].compareTo(a[pq[j]]);
         return  c * type > 0;
    }
    
     /**
     * 交換a[i]和a[j]
     * 
@param  i, j    第i, j個元素
     
*/
     private  void exch( int i,  int j){
         int t = pq[i];
        pq[i] = pq[j];
        pq[j] = t;
        qp[pq[i]] = i;
        qp[pq[j]] = j;
    }
    
     /**
     * 將a[k]向上移
     * 
@param  k    表示待移動的是a[k]
     * 函數將元素a[k]移動到正確的位置,使得a[k]
     * 比其子節點元素大。
     
*/
     private  void swim( int k){
         while(k > 1 && less(k / 2 , k)){
            exch(k, k / 2);
            k = k / 2;
        }
    }
    
     /**
     * 自頂向下堆化,將a[k]逐漸下移
     * 
@param  k    表示代移動的是a[k]a
     * 
@param  N    表示元素總個數為N
     * 函數將元素a[k]移動到正確的位置
     
*/
     private  void sink( int k,  int N){
         while(2 * k <= N){
             int j = 2 * k;
             if(j < N && less(j, j + 1)) j++;
             if(!less(k, j))  break;
            exch(k, j); 
            k = j;
        }
    }
    
     // 判斷當前隊列是否為空
     public  boolean empty(){
         return N == 0;
    }
    
     /**
     * 插入一個新的元素,插入的位置為v
     
*/
     public  void insert( int v){
        pq[++N] = v;
        qp[v] = N;
        swim(N);
    }
    
     /**
     * 獲取(刪除)當前最大的元素
     * 
@return  當前最大的元素
     
*/
     public  int getmax(){
        exch(1, N);
        sink(1, N - 1);
         return pq[N--];
    }
    
     //  改變第k個元素
     public  void change( int k){
        swim(qp[k]);
        sink(qp[k], N);
    }
    
     //  調整第k個元素在堆中的位置
     public  void lower( int k){
        swim(qp[k]);
    }
}

 

其中元素項數組a為指向隊列中處理的記錄對應的數組的指針,稱這里的數組為客戶數組。數組pq為指向用戶數組中元素的下標數組,堆中第i個位置處對應着客戶數組中第pq[i]個元素,用a[pq[i]]來訪問客戶數組中對應的元素。數組qp為客戶數組中各元素的優先級,qp[j]表示客戶數組中第j的元素項的優先級為qp[j],那么優先隊列中第i個位置對應的數組元素的優先級為pq[pq[i]]。在這里我們對堆中每個位置的優先級的量化為:隊列中第i個位置對應數組元素的優先級為i,也就是說qp[pq[i]]=i。這里有一個新的成員變量type,該變量決定了堆的類型。

在less函數中,首先應用函數compareTo比較客戶數組中兩個元素a[i]和a[j],比較結果為c。如果a[i]<a[j]則c<0,若此時type=-1,則c*type>0,less函數返回true;反之,若此時type=1,則c*type<0,less函數返回false。所以在type=-1時,這里的less函數與之前中討論的最大堆中的less函數功能相同。事實上,這里的type取值-1表示最大堆,反之取值1表示最小堆。由於less函數在處理隊列的其它函數中都有調用,下面我們以最大堆為例,即type=-1,進行討論。

函數swim將隊列中指定k位置對應的客戶數組的下標向上移動到正確的位置,直到其父節點處對應的元素值不比它小為止。sink函數的過程與之相反,是將k位置上的對應的下標向下移動到合適的位置。getmax函數返回隊列頂部對應的客戶數組的下標,在最大堆中,該下表對應着客戶數組中的最大項。

.3.2.2 PFS搜索增廣路徑

接下來將介紹一種Ford-Fulkerson算法的實現,該算法沿着可以使流得到最大增長的路徑進行擴充,可以利用基於下標堆的優先隊列來實現。在圖中的示例就是基於這個思路。在算法中用數wt記錄每個能提供的流(的相反數),數組st記錄與每個頂點相關聯的提供最大流的邊。算法的實現如下:

 

 

/**
 * 優先級優先遍歷函數;
 * 函數搜索網絡起點s至終點t的最大流路徑。
 
*/
private  boolean PFS(){
     int M = -1 * Integer.MAX_VALUE;
     //  基於下標堆(最小堆)的優先隊列
    intPQi pQ =  new intPQi(wt, 1);
     for( int v = 0; v < G.get_nv(); v++){
        wt[v] =  new ElemItem<Integer>(0);
        st[v] =  null;
        pQ.insert(v);
    }
     //  起點s置於優先隊列頂部
    wt[s] =  new ElemItem<Integer>(M);
    pQ.lower(s);
    
     //  迭代過程,尋找流量最大的路徑
     while(!pQ.empty()){
         //  堆頂頂點號,getmax返回最小
         int v = pQ.getmax();
        wt[v] =  new ElemItem<Integer>(M);
         //  v到達終點或者st[v]為空則推出迭代
         if(v == t)  break;
         if(v != s && st[v] ==  nullbreak;
         //  更新v的所有相鄰頂點在擴充路徑上的流
         for(NetworkEdge E = G.firstEdge(v); 
            G.isEdge(E); E = G.nextEdge(E)){
            NetworkEdge TmpE = E;
             //  如果E的容量為負,則將E更新為E的反向邊
             if(G.getEdgeC(E) < 0){
                E = G.getNetworkEdge(E.get_v2(), E.get_v1());
            }
             if(E ==  nullreturn  false;
             //  獲取E的另一頂點w
             int w = E.other(v);
             //  獲取頂點w在擴充路徑上的流
             int cap = G.capRto(E, w);
             int wt_v = ((Integer)(wt[v].getElem())).intValue();
             int P = cap < (-1 * wt_v)?cap:(-1 * wt_v);
             int wt_w = ((Integer)(wt[w].getElem())).intValue();
             if(cap > 0 && (-1 * P) < wt_w){
                 //  更新頂點w在擴充路徑上的流
                wt[w] =  new ElemItem<Integer>(-1 * P);
                 //  更新優先隊列
                pQ.lower(w);
                st[w] = E;
            }
            E = TmpE;
        }
    }
    System.out.println("--------------------------");
     for( int k = 0; k < st.length; k++ ){
         if(st[k] !=  null)
            System.out.println(st[k].get_v1() 
                    + "-" + st[k].get_v2());
    }
     return st[t] !=  null;
}

 

算法中利用的下標堆優先隊列中使用了最小堆,隊列的客戶數組為wt,算法按照能提供的流由大到小的順序取出隊列中的頂點v。獲取所有與頂點v相關聯的邊,這些邊不僅包括以v為源點的邊,還包括以v為終點的邊。然后對每條邊上的另一頂點w(相對於頂點v)所能提供的流的大小wt[w]以及對應的邊st[w]。一旦頂點v找不到相關聯的邊則函數返回false,即找不到增廣路徑。

在流網絡中。訪問與頂點v相關聯的邊時,我們只能通過firstEdgenextEdge迭代訪問以頂點v為源點的邊。但是在算法中我們還需要訪問以v為終點的邊,這需要對原始流網絡做技巧性的調整。我們給原始流網絡中的每一條邊預留一條反向的邊,這條邊的容量為-1。如果源點為v的某條邊E的容量G.getEdgeC(E) < 0,則將邊E反向即可獲得對應的以v為終點的邊。

.3.3 流增廣過程

基於PFS搜索得到的st數組,我們可以得到各個頂點相關聯的能提供最大流的邊,根據這些邊形成的增廣路徑可以增加網絡流。流網絡源點為s,匯點為t,則從t開始,更新邊(st[t], t),然后繼續向頂點s迭代直到到達頂點s。算法實現如下:

 

private  void augument(){
     int d = G.capRto(st[t], t);
     for( int v = ST(t); v != s; 
            v = ST(v)){
         int tt = G.capRto(st[v], v);
         if(G.capRto(st[v], v) < d)
            d = G.capRto(st[v], v);
    }
    
    G.addflowRto(st[t], t, d);
     for( int v = ST(t); v != s; v = ST(v))
        G.addflowRto(st[v], v, d);
    
}

 

 

.3.4 基於PFS的Ford-Fulkerson算法

結合PFS搜索過程和流增廣過程可以實現高效的Ford-Fulkerson方法。該算法沿着可以使流得到最大增長的路徑進行擴充。實現如下:

 

public  void Ford_Fulkerson(){
     //  迭代
     while(PFS()){ 
        augument();
         //  打印當前網絡各邊的網絡流
         for( int i = 0; i < G.get_nv(); i++){
             for(NetworkEdge E = G.firstEdge(i); 
                G.isEdge(E); E = G.nextEdge(E)){
                 if(G.getEdgeFlow(E) > 0)
                    System.out.print(E.get_v1() + 
                        " <-- " + G.getEdgeFlow(E) +
                        "/" +G.getEdgeC(E) + 
                        " --> " + E.get_v2() + " ||\t");
            }
            System.out.println();
        }
        
    }
}

 

算法中迭代地運用PFS搜索殘留圖中的增廣路徑,並調用增廣過程不斷增加網絡的流。為例,編寫測試程序:

 

public  class NetworkExample {
     public  static  void main(String args[]){
        Network N =  new Network(6);
        N.setEdgeC(0, 1, 2);
        N.setEdgeC(0, 2, 3);
        N.setEdgeC(1, 3, 3);
        N.setEdgeC(1, 4, 1);
        N.setEdgeC(2, 3, 1);
        N.setEdgeC(2, 4, 1);
        N.setEdgeC(3, 5, 2);
        N.setEdgeC(4, 5, 3);
        
        N.setEdgeC(1, 0, -1);
        N.setEdgeC(2, 0, -1);
        N.setEdgeC(3, 1, -1);
        N.setEdgeC(4, 1, -1);
        N.setEdgeC(3, 2, -1);
        N.setEdgeC(4, 2, -1);
        N.setEdgeC(5, 3, -1);
        N.setEdgeC(5, 4, -1);
        
        NetworkMaxFlow NF =  new NetworkMaxFlow(N, 0, 5);
        NF.Ford_Fulkerson();
    }
}

 

    程序運行結果為:
    
    PFS搜索得到的增廣路徑的邊:
    0-1
    0-2
    1-3
    2-4
    3-5
    當前每條邊上的流/容量:
    0 <-- 2/2 --> 1 ||    
    1 <-- 2/3 --> 3 ||    
    
    3 <-- 2/2 --> 5 ||    
    
    
    PFS搜索得到的增廣路徑的邊:
    1-3
    0-2
    2-3
    2-4
    4-5
    當前每條邊上的流/容量:
    0 <-- 2/2 --> 1 ||    0 <-- 1/3 --> 2 ||    
    1 <-- 2/3 --> 3 ||    
    2 <-- 1/1 --> 4 ||    
    3 <-- 2/2 --> 5 ||    
    4 <-- 1/3 --> 5 ||    
    
    PFS搜索得到的增廣路徑的邊:
    1-3
    0-2
    2-3
    1-4
    4-5
    當前每條邊上的流/容量:
    0 <-- 2/2 --> 1 ||    0 <-- 2/3 --> 2 ||    
    1 <-- 1/3 --> 3 ||    1 <-- 1/1 --> 4 ||    
    2 <-- 1/1 --> 3 ||    2 <-- 1/1 --> 4 ||    
    3 <-- 2/2 --> 5 ||    
    4 <-- 2/3 --> 5 ||    
    
    PFS搜索得到的增廣路徑的邊:
    0-2

 

從結果可以看出,進過三次搜索便可以找出流網絡中的最大流。每次都打印顯示PFS搜索得到的增量路徑以及網絡中每條邊上的流。

再以為例,驗證基於PFS算法的Ford-Fulkerson算法可以更高效:

    Network N =  new Network(4);
    N.setEdgeC(0, 1, 100);
    N.setEdgeC(0, 2, 100);
    N.setEdgeC(1, 2, 1);
    N.setEdgeC(1, 3, 100);
    N.setEdgeC(2, 3, 100);
    
    N.setEdgeC(1, 0, -1);
    N.setEdgeC(2, 0, -1);
    N.setEdgeC(2, 1, -1);
    N.setEdgeC(3, 1, -1);
    N.setEdgeC(3, 2, -1);
    
    NetworkMaxFlow NF =  new NetworkMaxFlow(N, 0, 3);
    NF.Ford_Fulkerson();
    
    PFS搜索得到的增廣路徑的邊:
    0-1
    0-2
    1-3
    當前每條邊上的流/容量:
    0 <-- 100/100 --> 1 ||    
    1 <-- 100/100 --> 3 ||    
    
    
    PFS搜索得到的增廣路徑的邊:
    0-2
    2-3
    當前每條邊上的流/容量:
    0 <-- 100/100 --> 1 ||    0 <-- 100/100 --> 2 ||    
    1 <-- 100/100 --> 3 ||    
    2 <-- 100/100 --> 3 ||    
    
    PFS搜索得到的增廣路徑的邊:

 

 

    可見,基於PFS算法的Ford-Fulkerson方法的實現比基於DFS的實現效率更高。事實上,可以證明基於PFS算法的實現中所需要的增廣路徑搜索次數最多為V·E/2,而普通的Ford-Fulkerson方法所需的增廣路徑的搜索次數最多為V·M,其中M為流網絡中最大的邊容量,通常需要的搜索次數更多。

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM