尋找最大流
在大規模戰爭中,后勤補給是重中之重,為了盡最大可能滿足前線的物資消耗,后勤部隊必然要充分利用每條運輸網,這正好可以用最大流模型解決。如何尋找一個復雜網絡上的最大流呢?
直覺上的方案
一種直覺上的方案是在一個流網絡找到一條從源點到匯點的未充分利用的有向路徑,然后增加該路徑的流量,反復迭代,直到沒有這樣的路徑為止。廣度優先搜索可以在一個流網絡中找到這樣的路徑,這種路徑一旦被開充分利用,就會因為達到了最大流量而被“填滿”,下次不必再打這條路徑的主意。問題是,這樣做就一定會得到最大流嗎?考慮圖下面的網絡。
圖1
兩條明顯的路徑是v1→v2→v4→v6和v1→v3→v5→v6,依次“填滿”兩條路徑:
圖2
此時已經無法再找到新的路徑,因此判斷最大流是3。然而3並不是最大流,真正的最大流是4:
圖3
看來尋找最大流並沒有那么簡單。為了應對這種情況,需要引入殘存網的概念。
殘存網
殘存網也叫余留網、剩余網,它由原網絡中沒有被充分利用的邊構成。假設有一個流網絡G和它的網絡流f,G的殘存網用Gf表示,我們這樣構造一個初始的Gf:Gf和G有同樣的頂點,對於原網絡中的各條邊,Gf將有1條或2條邊與之對應,每條邊只記錄了容量。對於G的一條邊v→w,C(v→w)和f(v→w)代表了該邊的容量和流量,如果f(v→w)的值為正,則在殘存網中包含了一條容量為f(v→w)的邊w→v,這條邊是原網絡中沒有的逆向邊;如果f(v→w)小於C(v→w),則在殘存網中會一條容量為C(v→w)- f(v→w)的邊v→w,這條邊與原網絡同向,它的容量是原網絡中v→w的剩余容量;如果原網絡中v→w是滿邊,則殘存網中不存在v→w。圖8.9展示了一個流網絡對應的殘存網。
圖4
殘存網中只記錄容量,不記錄流量,流量是通過逆向邊的容量記錄的。由於在原網絡中v4→v6是滿邊,所以殘存網中不存在v4→v6,相當於v4→v6的剩余容量用光了,即Cf(v4→v6)=0。由於殘存網和原網絡存在對應關系,所以增加原網絡的流量相當於調整殘存網。
增廣路徑
增廣路徑是殘存網中一條連接源點和匯點的簡單有向路徑,也稱為擴充路徑。上圖中v1→v3→v5→v6就是一條增廣路徑。
一條增廣路徑代表着原網絡中一條尚未被充分利用的路徑,如果想讓這條路徑得到充分利用,勢必會把增廣路徑上的一條邊的剩余容量用完,這樣一來,殘存網至少會有一條邊消失,或直接調轉方向:
圖5
Gf1上v3→v5的剩余容量被用完,所以在Gf2上刪除v3→v5,並增加1條反向的邊v5→v3,同時增加另外2條反向邊v3→v1和v6→v5,並更新v1→v3和v5→v6的剩余容量。只要增廣路徑v1→v3→v5→v6得到充分利用,那么原網絡上的相應路徑也將得到充分利用:
圖6
可以看出,原網絡的v3→v5已經變成了滿邊,此時v1→v3→v5→v6也不存在繼續擴充的余地。
增廣路徑在告訴我們一個結論,只要把殘存網上的增廣路徑用完,原網絡就無法繼續擴充,意味着得到了最大網絡流。現在,圖5殘存網Gf2中似乎沒有一條連接源點和匯點的路徑了,如果就這樣結束,則仍然無法找到最大流,怎么辦呢?別忘了,殘存網中還有逆向邊,因此還有一條增廣路徑,這就是v1→v3→v4→v2→v5→v6,我們填滿該路徑。
圖7
在Gf2中,有1個單位的流量流過v4→v2,這相當於把原來流經v2→v4的流量退還回去,從而獲得把退還的流量分配到其他路徑的能力。當填滿所有的增廣路徑時,殘存網中將不存在從源點到匯點的有向路徑,此時原網絡中的流值也達到了最大:
圖8
增廣路徑是一條簡單路徑,路徑中的每個頂點只能出現一次,並不是每條連接源點和匯點的有向路徑都是增廣路徑,例如在8.9中,v1→v2→v5是增廣路徑,v1→v2→v3→v4→v2→v5雖然也連通了源點和匯點,但是v2中這條路徑上出現了2次,這條路徑並不“簡單”,因此不是增廣路徑:
圖9
為什么定義增廣路徑必須是簡單路徑呢?以圖9的v1→v2→v3→v4→v2→v5為例,設這條路徑為P,石油先流入中轉站v2,然后繞了一圈后有回到v2,最終統一由v2流向v5。對於v1→v2和v2→v5的容量,無非是兩種可能,C(v1→v2)<=C(v2→v5)或C(v1→v2)>C(v2→v5)。
當C(v1→v2)<=C(v2→v5)時,P上能夠擴充的流量取決於P上容量最小的邊,因此最終擴充的流量一定小於等於C(v1→v2),如果最終擴充的流量小於C(v1→v2),那么v1→v2並沒有得到充分利用,中下一次尋徑中還會再次找到v1→v2→v5,這還不如一開始就通過v1→v2→v5擴充;與此類似如果最終擴充的流量等於C(v1→v2),也不如一開始就通過v1→v2→v5擴充來得方便。同理,當C(v1→v2)>C(v2→v5)時, 最快的擴充途徑仍然是通過v1→v2→v5擴充。可以看出,非簡單路徑並不是無法得到最大流,只是這樣做會增加搜索路徑的次數,徒耗錢糧。
增廣路徑最大流算法
增廣路徑最大流算法也稱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位的