網絡流(2)——用Ford-Fullkerson算法尋找最大流 搜索的策略(3)——覲天寶匣上的拼圖

尋找最大流

  在大規模戰爭中,後勤補給是重中之重,爲了盡最大可能知足前線的物資消耗,後勤部隊必然要充分利用每條運輸網,這正好能夠用最大流模型解決。如何尋找一個複雜網絡上的最大流呢?html

直覺上的方案

  一種直覺上的方案是在一個流網絡找到一條從源點到匯點的未充分利用的有向路徑,而後增長該路徑的流量,反覆迭代,直到沒有這樣的路徑爲止。廣度優先搜索能夠在一個流網絡中找到這樣的路徑,這種路徑一旦被開充分利用,就會由於達到了最大流量而被「填滿」,下次沒必要再打這條路徑的主意。問題是,這樣作就必定會獲得最大流嗎?考慮圖下面的網絡。node

  圖1算法

  兩條明顯的路徑是v1v2v4v6v1v3v5v6,依次「填滿」兩條路徑:網絡

  圖2post

  此時已經沒法再找到新的路徑,所以判斷最大流是3。然而3並非最大流,真正的最大流是4:學習

  圖3url

  看來尋找最大流並無那麼簡單。爲了應對這種狀況,須要引入殘存網的概念。spa

殘存網

  殘存網也叫餘留網、剩餘網,它由原網絡中沒有被充分利用的邊構成。假設有一個流網絡G和它的網絡流fG的殘存網用Gf表示,咱們這樣構造一個初始的GfGfG有一樣的頂點,對於原網絡中的各條邊,Gf將有1條或2條邊與之對應,每條邊只記錄了容量。對於G的一條邊vwC(vw)和f(vw)表明了該邊的容量和流量,若是f(vw)的值爲正,則在殘存網中包含了一條容量爲f(vw)的邊wv,這條邊是原網絡中沒有的逆向邊;若是f(vw)小於C(vw),則在殘存網中會一條容量爲C(vw)- f(vw)的邊vw,這條邊與原網絡同向,它的容量是原網絡中vw的剩餘容量;若是原網絡中vw是滿邊,則殘存網中不存在vw。圖8.9展現了一個流網絡對應的殘存網。3d

  圖4code

  殘存網中只記錄容量,不記錄流量,流量是經過逆向邊的容量記錄的。因爲在原網絡中v4v6是滿邊,因此殘存網中不存在v4v6,至關於v4v6的剩餘容量用光了,即Cf(v4v6)=0。因爲殘存網和原網絡存在對應關係,因此增長原網絡的流量至關於調整殘存網。

增廣路徑

  增廣路徑是殘存網中一條鏈接源點和匯點的簡單有向路徑,也稱爲擴充路徑。上圖中v1v3v5v6就是一條增廣路徑。

  一條增廣路徑表明着原網絡中一條還沒有被充分利用的路徑,若是想讓這條路徑獲得充分利用,勢必會把增廣路徑上的一條邊的剩餘容量用完,這樣一來,殘存網至少會有一條邊消失,或直接調轉方向:

  圖5

  Gf1v3v5的剩餘容量被用完,因此在Gf2上刪除v3v5,並增長1條反向的邊v5v3,同時增長另外2條反向邊v3v1v6v5,並更新v1v3v5v6的剩餘容量。只要增廣路徑v1v3v5v6獲得充分利用,那麼原網絡上的相應路徑也將獲得充分利用:

  圖6

  能夠看出,原網絡的v3v5已經變成了滿邊,此時v1v3v5v6也不存在繼續擴充的餘地。

  增廣路徑在告訴咱們一個結論,只要把殘存網上的增廣路徑用完,原網絡就沒法繼續擴充,意味着獲得了最大網絡流。如今,圖5殘存網Gf2中彷佛沒有一條鏈接源點和匯點的路徑了,若是就這樣結束,則仍然沒法找到最大流,怎麼辦呢?別忘了,殘存網中還有逆向邊,所以還有一條增廣路徑,這就是v1v3v4v2v5v6,咱們填滿該路徑。

  圖7

  在Gf2中,有1個單位的流量流過v4v2,這至關於把原來流經v2v4的流量退還回去,從而得到把退還的流量分配到其餘路徑的能力。當填滿全部的增廣路徑時,殘存網中將不存在從源點到匯點的有向路徑,此時原網絡中的流值也達到了最大:

  圖8

  增廣路徑是一條簡單路徑,路徑中的每一個頂點只能出現一次,並非每條鏈接源點和匯點的有向路徑都是增廣路徑,例如在8.9中,v1v2v5是增廣路徑,v1v2v3v4v2v5雖然也連通了源點和匯點,可是v2中這條路徑上出現了2次,這條路徑並不「簡單」,所以不是增廣路徑:

  圖9

  爲何定義增廣路徑必須是簡單路徑呢?以圖9的v1v2v3v4v2v5爲例,設這條路徑爲P,石油先流入中轉站v2,而後繞了一圈後有回到v2,最終統一由v2流向v5。對於v1v2v2v5的容量,無非是兩種可能,C(v1v2)<=C(v2v5)或C(v1v2)>C(v2v5)。

  當C(v1v2)<=C(v2v5)時,P上可以擴充的流量取決於P上容量最小的邊,所以最終擴充的流量必定小於等於C(v1v2),若是最終擴充的流量小於C(v1v2),那麼v1v2並無獲得充分利用,中下一次尋徑中還會再次找到v1v2v5,這還不如一開始就經過v1v2v5擴充;與此相似若是最終擴充的流量等於C(v1v2),也不如一開始就經過v1v2v5擴充來得方便。同理,當C(v1v2)>C(v2v5)時, 最快的擴充途徑仍然是經過v1v2v5擴充。能夠看出,非簡單路徑並非沒法獲得最大流,只是這樣作會增長搜索路徑的次數,徒耗錢糧。

增廣路徑最大流算法

  增廣路徑最大流算法也稱Ford-Fullkerson算法,它經過不斷尋找並填滿殘存網中的增廣路徑來擴充原網絡的流值,直到殘存網中不存在增廣路徑爲止:

  每填充一條增廣路徑,就會有這至少一條邊被刪除或掉轉方向,在實際應用中,對於刪除的邊僅僅是將其容量清零,而並不是真正將這條邊刪除。經過擴展Edge類使之可以表達殘存邊。

 1 class Edge():
 2     ''' 流網絡中的邊 '''
 3     def __init__(self, v, w, cap, flow=0):
 4         '''
 5         定義一條邊 v→w
 6         :param v: 起點
 7         :param w: 終點
 8         :param cap: 容量
 9         :param flow: v→w上的流量
10         '''
11         self.v, self.w, self.cap, self.flow = v, w, cap, flow
12
13     def other_node(self, p):
14         ''' 返回邊中與p相對的另外一頂點 '''
15         return self.v if p == self.w else self.w
16
17     def residual_cap_to(self, p):
18         '''
19         計算殘存邊的剩餘容量
20         若是p=w,residual_cap_to(p)返回 v→w 的剩餘容量
21         若是p=v,residual_cap_to(p)返回 w→v 的剩餘容量
22         '''
23         return self.cap - self.flow if p == self.w else self.flow
24
25     def moddify_flow(self, p, x):
26         ''' 將邊的流量調整x '''
27         if p == self.w: # 若是 p=w,將v→w的流量增長x
28             self.flow += x
29         else: #  不然將v→w的流量減小x
30             self.flow -= x
31
32     def __str__(self):
33         return str(self.v) + '' + str(self.w)

  每條邊有兩個節點,若是一條邊是v→w,根據傳入的頂點不一樣,residual_cap_to方法既能夠表示Cf(v→w)又能夠表示Cf(w→v)。

  因爲殘存網的兩個頂點間可能存在兩條邊,所以在Network類中添加edges方法用來取得鏈接某一頂點的全部邊,包括該頂點的流出邊和流入邊。

 1 class Network():
 2     ''' 流網絡 '''
 3     def __init__(self, V:list, E:list, s:int, t:int):
 4         '''
 5         :param V: 頂點集
 6         :param E: 邊集
 7         :param s: 原點
 8         :param t: 匯點
 9         :return:
10         '''
11         self.V, self.E, self.s, self.t = V, E, s, t
12
13     def edges_from(self, v):
14         ''' 從v頂點流出的邊 '''
15         return [edge for edge in self.E if edge.v == v]
16
17     def edges_to(self, v):
18         ''' 流入v頂點的邊 '''
19         return [edge for edge in self.E if edge.w == v]
20
21     def edges(self, v):
22         ''' 鏈接v頂點的全部邊 '''
23         return self.edges_from(v) + self.edges_to(v)
24
25     def flows_from(self, v):
26         '''v頂點的流出量 '''
27         edges = self.edges_from(v)
28         return sum([e.flow for e in edges])
29
30     def flows_to(self, v):
31         ''' v頂點的流入量 '''
32         edges = self.edges_to(v)
33         return sum([e.flow for e in edges])
34
35     def check(self):
36         ''' 源點的流出是否等於匯點的流入 '''
37         return self.flows_from(self.s) == self.flows_to(self.t)
38
39     def display(self):
40         if self.check() is False:
41             print('該網絡不符合守恆定律')
42             return
43         print('%-10s%-8s%-8s' % ('', '容量', ''))
44         for e in self.E:
45             print('%-10s%-10d%-8s' %
46                   (e, e.cap,e.flow if e.flow < e.cap else str(e.flow) + '*'))

  接下來經過FordFulkerson類計算網絡中的最大流:

 1 class FordFulkerson():
 2     def __init__(self, G:Network):
 3         self.G = G
 4         self.max_flow = 0  # 最大流
 5
 6     class Node:
 7         ''' 用於記錄路徑的軌跡 '''
 8         def __init__(self, w, e:Edge, parent):
 9             '''
10             :param w: 頂點
11             :param e: 從上一頂點流入w的邊
12             :param parent: 上一頂點
13             '''
14             self.w, self.e, self.parent = w, e, parent
15
16     def get_augment_path(self):
17         ''' 獲取網絡中的一條增廣路徑 '''
18         path = None
19         visited = set() # 被訪問過的頂點
20         visited.add(self.G.s)
21         q = Queue()
22         q.put(self.Node(self.G.s, None, -1))
23         while not q.empty():
24             node_v = q.get()
25             v = node_v.w
26             for e in self.G.edges(v): # 遍歷鏈接v的全部邊
27                 w = e.other_node(v) # 邊的另外一頂點,e的指向是v→w
28                 # v→w有剩餘容量且w沒有被訪問過
29                 if e.residual_cap_to(w) > 0 and w not in visited:
30                     visited.add(w)
31                     node_w = self.Node(w, e, node_v)
32                     q.put(node_w)
33                     if w == self.G.t: # 到達了匯點
34                         path = node_w
35                         break
36         return path
37
38     def start(self):
39         ''' 增廣路徑最大流算法主體方法 '''
40         while True:
41             path = self.get_augment_path() # 找到一條增廣路徑
42             if path is None:
43                 break
44             bottle = 10000000 # 增廣路徑的瓶頸
45             node = path
46             while node.parent != -1: # 計算增廣路徑上的最小剩餘量
47                 w, e = node.w, node.e
48                 bottle = min(bottle, e.residual_cap_to(w))
49                 node = node.parent
50             node = path
51             while node.parent != -1: # 修改殘存網
52                 w, e = node.w, node.e
53                 e.moddify_flow(w, bottle)
54                 node = node.parent
55             self.max_flow += bottle # 擴充最大流
56
57     def display(self):
58         print('最大網絡流 = ', self.max_flow)
59         print('%-10s%-8s%-8s' % ('', '容量', ''))
60         for e in self.G.E:
61             print('%-10s%-10d%-8s' %
62                   (e, e.cap, e.flow if e.flow < e.cap else str(e.flow) + '*'))

  get_augment_path和《搜索的策略(3)——覲天寶匣上的拼圖》 中的bfs方法相似,用先進先出隊列實現廣度優先搜索,找到殘存網中的一條增廣路徑,並經過visited記錄訪問過的節點,以確保路徑是一條最簡路徑,Node用於記錄路徑中經歷的節點,start()實現了主體代碼。

  下面的代碼用於尋找圖1的最大流:

1 V = [1, 2, 3, 4, 5, 6]
2 E = [Edge(1, 2, 2), Edge(1, 3, 3), Edge(2, 4, 3), Edge(2, 5, 1),
3      Edge(3, 4, 1), Edge(3, 5, 1), Edge(4, 6, 2), Edge(5, 6, 3)]
4 s, t = 1, 6
5 G = Network(V, E, s, t)
6 ford_fullkerson = FordFulkerson(G)
7 ford_fullkerson.start()
8 ford_fullkerson.display()

  運行結果:

  下章內容:最小st-剪切,切斷敵軍的補給線


   做者:我是8位的

  出處:http://www.cnblogs.com/bigmonkey

  本文以學習、研究和分享爲主,如需轉載,請聯繫本人,標明做者和出處,非商業用途! 

  掃描二維碼關注公衆號「我是8位的」

相關文章
相關標籤/搜索