原文連接https://www.cnblogs.com/luweiseu/archive/2012/07/14/2591573.htmlhtml
做者:wlugit
7. 網絡流算法--Ford-Fulkerson方法及其多種實現
網絡流
在上一章中咱們討論的主題是圖中頂點之間的最短路徑,例如公路地圖上兩地點之間的最短路徑,因此咱們將公路地圖抽象爲有向帶權圖。本章咱們將對基於有向帶權圖的模型作進一步擴展。算法
不少系統中涉及流量問題,例如公路系統中車流量,網絡中的數據信息流,供油管道的油流量等。咱們能夠將有向圖進一步理解爲「流網絡」(flow network),並利用這樣的抽象模型求解有關流量的問題。shell
![](http://static.javashuo.com/static/loading.gif)
圖 電路原理圖可抽象爲網絡流編程
流網絡中每條有向邊能夠認爲是傳輸物質的管道,每一個管道有固定的容量,能夠看做是物質可以流經該管道的最大速度。頂點是管道之間的交叉鏈接點,除了匯點以外,物質只流經這些點,不會再頂點滯留或消耗。也就是說,物質進入某頂點的速度必須等於離開該頂點的速度。這一特性被稱爲「流守恆」(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,知足一下性質:網絡
l 容量限制:對全部頂點對u,v∈V,知足f(u, v) ≦ c(u, v);數據結構
l 反對稱性:對全部頂點對u,v∈V,知足f(u, v) = - f(v, u);
l 流守恆性:對全部頂點對u∈V-{s, t},知足Σv∈Vf(u,v)=0。
f(u, v)稱爲從頂點u到頂點v的流,流的值定義爲:
|f| =Σv∈Vf(s,v),
即從源點s出發的總的流。
在最大流問題中,咱們須要求解源點s到匯點t之間的最大流f(s, t),同時咱們還但願瞭解達到該值的流。對於一個指定的源點s和指定匯點t的網,咱們稱之爲st-網。
如圖所示爲一個流網絡,其中頂點之間的邊的粗細對應着邊的容量大小。
![](http://static.javashuo.com/static/loading.gif)
圖 有向圖表示網絡流
下面以圖爲例,在流的三個性質條件下嘗試性地尋找圖中的最大流,如圖(a~c)。
![](http://static.javashuo.com/static/loading.gif)
![](http://static.javashuo.com/static/loading.gif)
從上圖(a~c)中能夠發現,流網絡從源點s流出的量依次爲2,3,5,而流入匯點t的流量也2,3,5。事實上,任何流從s流出量總應該等於到匯點t的流入量,下面對這一命題作簡單證實。
![](http://static.javashuo.com/static/loading.gif)
構造:如圖(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):
l 若是f(u, v) > 0,則在殘留網中包含一條容量爲f(u, v)的邊(v, u);
l 若是f(u, v) < c(u, v),則在殘留網中包含一條容量爲c(u, v) - f(u, v)的邊(u, v)。
殘留網容許咱們使用任何廣義圖搜索算法來找一條增廣路徑,由於殘留網中從源點s到匯點t的路徑都直接對應着一條增廣路徑。以圖爲例,具體分析增廣路徑及其相應殘留網,如圖(a~d)。
![](http://static.javashuo.com/static/loading.gif)
(a)原始圖流網絡,每條邊上的流都爲0。由於f(u, v) = 0 < c(u, v),則在殘留網中包含容量爲c(u, v)的邊(u, v),因此此時殘留圖中頂點與原始流網絡相同,邊也與原始流網絡相同,而且邊的容量與原始流網絡相同。
在殘留網中能夠找到一條增廣路徑<v0, v1, v3, v5>,每條邊的流爲2,此原始流網絡和殘留網中相應的邊會有所變化,以下圖。
![](http://static.javashuo.com/static/loading.gif)
(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,此原始流網絡和殘留網會有所變化,以下圖。
![](http://static.javashuo.com/static/loading.gif)
(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,此原始流網絡和殘留網會有所變化,以下圖。
![](http://static.javashuo.com/static/loading.gif)
(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) = Σu∈SΣv∈T f(u, v)表示,割(S, T)的容量用C(S, T) = Σu∈SΣv∈T c(u, v)。如圖,流網絡的一個割爲({s, v1, v2},{v3, v4, t})
![](http://static.javashuo.com/static/loading.gif)
圖 (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)的流量,因此,割的流等於流網絡的流的值。
![](http://static.javashuo.com/static/loading.gif)
在上圖中,計算割(S, T)的流量時f3的提供正的流量值,而f2提供的是負的流量值,而且在計算割的容量時只有提供流量f3的邊的容量參與相加,根據流的第一條性質,f3的值不會大於割的容量,因此:
f(S, T) = f3 – f2 ≦ f3 ≦ C(S, T)。
因爲流網絡中全部割的流都相等而且等於網絡的流,全部網絡的任何流的值都不大於任何一個割的容量。
根據上面對流網絡的中割的概念的介紹,下面引入最大流最小割定理,並利用該定理說明Ford-Fulkerson算法的正確性。
最大流最小割定理:一個網中全部流中的最大值等於全部割中的最小容量。而且能夠證實一下三個條件等價:
l f是流網絡G的一個最大流;
l 殘留網Gf不包含增廣路徑;
l 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的路徑,則t∉S,因此獲得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次搜索才能獲得最大流。
![](http://static.javashuo.com/static/loading.gif)
若是換一種方法對殘留網中的進行遍歷將會很快求得流網絡的最大流。如圖,第一次在頂點1搜索下一條邊時,不是選擇邊(1, 2)而是選擇容量更大的邊(1, t);第二次在頂點2處搜索下一條邊時,選擇邊(2, t)。這樣只要兩次遍歷便可求解最大流。可見,在殘留網中搜索增廣路徑的算法直接影響Ford-Fulkerson方法實現的效率。
![](http://static.javashuo.com/static/loading.gif)
3.1 流網絡數據結構
.3.1.1 流網絡邊的數據結構
流網絡數據結構與圖數據結構比較類似,首先也須要設計流網絡的邊的數據結構。這裏咱們只討論基於鏈接表的流網絡數據結構的實現。在圖數據結構中邊包含了源點、終點以及邊所在其對應鏈表中的節點的指針。
流網絡邊種一樣包含上述三個成員,但還包括其它針對流網絡算法的成員函數,其實現以下:
// 私有成員變量
// 邊的源頂點和終節點
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 流網絡數據結構
流網絡數據結構的鏈接表法的實現與圖數據結構相似。須要定義一個鏈表來存放與給定頂點相鄰的頂點,以及這兩個頂點造成的邊的信息,在流網絡中,邊的信息包括邊的容量和邊的流量。因此鏈表的節點設計爲:
// 私有成員,終點、權重、流
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比較相同源點的兩條邊的容量。
基於流網絡邊和鏈表節點數據結構,流網絡數據結構的實現以下:
// 私有成員變量
// 頂點鏈表數組,數組的每一個元素對應於
// 與頂點相連的全部頂點造成的鏈表
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() == null) return null;
return new NetworkEdge(v,
((NetworkLLinkNode)(vertexList[v].getCurrVal()
.getElem())).get_des(),
vertexList[v].currNode());
}
public NetworkEdge nextEdge(NetworkEdge w) {
if(w == null) return null;
int v = w.get_v1();
vertexList[v].setCurr(w.get_lk());
vertexList[v].next();
if(vertexList[v].getCurrVal() == null) return null;
return new NetworkEdge(v,
((NetworkLLinkNode)(vertexList[v].getCurrVal()
.getElem())).get_des(),
vertexList[v].currNode());
}
public boolean isEdge(NetworkEdge w) {
if(w == null) return 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 == null) return -1;
return w.get_v1();
}
public int edge_v2(NetworkEdge w) {
if(w == null) return -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 != null) return 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 基於下標堆的優先隊列
本節首先介紹基於下標對的優先隊列數據結構。假設要在優先隊列中處理的記錄在一個已存在的數組中,可讓優先隊列例程經過數組下標來引用數據項。這樣隊列中只須要數組的下標,全部對優先隊列的操做都是對數組下標的操做。這裏之因此要討論這種優先隊列,主要是由於在圖數據結構中咱們使用頂點的標號來訪問頂點,咱們能夠將頂點的標號做爲優先隊列中的元素項,經過這種映射方式能夠更高效地利用優先隊列處理有向帶權圖。
這裏基於下標的優先隊列與前面章節中討論的優先隊列的基本操做相似,讀者能夠溫習一下前面關於堆和優先隊列的內容。基於下標的優先隊列的實現以下:
// 存放元素內容的數組
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] == null) break;
// 更新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 == null) return 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相關聯的邊時,咱們只能經過firstEdge和nextEdge迭代訪問以頂點v爲源點的邊。可是在算法中咱們還須要訪問以v爲終點的邊,這須要對原始流網絡作技巧性的調整。咱們給原始流網絡中的每一條邊預留一條反向的邊,這條邊的容量爲-1。若是源點爲v的某條邊E的容量G.getEdgeC(E) < 0,則將邊E反向便可得到對應的以v爲終點的邊。
.3.3 流增廣過程
基於PFS搜索獲得的st數組,咱們能夠獲得各個頂點相關聯的能提供最大流的邊,根據這些邊造成的增廣路徑能夠增長網絡流。流網絡源點爲s,匯點爲t,則從t開始,更新邊(st[t], t),而後繼續向頂點s迭代直到到達頂點s。算法實現以下:
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方法。該算法沿着可使流獲得最大增加的路徑進行擴充。實現以下:
// 迭代
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 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算法能夠更高效:
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爲流網絡中最大的邊容量,一般須要的搜索次數更多。