1. 圖
1.1. 概念
- 頂
- 頂點的度 d
- 邊
- 相鄰
- 重邊
- 環
- 完全圖: 所有頂都相鄰
- 二分圖:
, X中, Y 中任兩頂不相鄰
- 軌道
- 圈
1.1.1. 性質
- G是二分圖
G無奇圈
- 樹是無圈連通圖
- 樹中,
1.2. 圖的表示
- 鄰接矩陣
-
鄰接鏈表
1.3. 樹
無圈連通圖, , 詳細見樹,
2. 搜索--求圖的生成樹[1]
2.1. BFS
for v in V:
v.d = MAX
v.pre = None
v.isFind = False
root. isFind = True
root.d = 0
que = [root]
while que !=[]:
nd = que.pop(0)
for v in Adj(nd):
if not v.isFind :
v.d = nd.d+1
v.pre = nd
v.isFind = True
que.append(v)
時間復雜度
2.2. DFS
def dfs(G):
time = 0
for v in V:
v.pre = None
v.isFind = False
for v in V : # note this,
if not v.isFind:
dfsVisit(v)
def dfsVisit(G,u):
time =time+1
u.begin = time
u.isFind = True
for v in Adj(u):
if not v.isFind:
v.pre = u
dfsVisit(G,v)
time +=1
u.end = time
begin, end 分別是結點的發現時間與完成時間
2.2.1. DFS 的性質
- 其生成的前驅子圖
形成一個由多棵樹構成的森林, 這是因為其與 dfsVisit 的遞歸調用樹相對應
-
括號化結構
- 括號化定理:
考察兩個結點的發現時間與結束時間的區間 [u,begin,u.end] 與 [v.begin,v.end]- 如果兩者沒有交集, 則兩個結點在兩個不同的子樹上(遞歸樹)
- 如果 u 的區間包含在 v 的區間, 則 u 是v 的后代
2.3. 拓撲排序
利用 DFS, 結點的完成時間的逆序就是拓撲排序
同一個圖可能有不同的拓撲排序
2.4. 強連通分量
在有向圖中, 強連通分量中的結點互達
定義 為
中所有邊反向后的圖
將圖分解成強連通分量的算法
在 Grev 上根據 G 中結點的拓撲排序來 dfsVisit, 即
compute Grev
initalization
for v in topo-sort(G.V):
if not v.isFind: dfsVisit(Grev,v)
然后得到的DFS 森林(也是遞歸樹森林)中每個樹就是一個強連通分量
3. 最小生成樹
利用了貪心算法,
3.1. Kruskal 算法
總體上, 從最開始 每個結點就是一顆樹的森林中(不相交集合, 并查集), 逐漸添加不形成圈的(兩個元素不再同一個集合),最小邊權的邊.
edges=[]
for edge as u,v in sorted(G.E):
if find-set(u) != find-set(v):
edges.append(edge)
union(u,v)
return edges
如果并查集的實現采用了 按秩合并與路徑壓縮技巧, 則 find 與 union 的時間接近常數
所以時間復雜度在于排序邊, 即 , 而
, 所以
, 時間復雜度為
3.2. Prim 算法
用了 BFS, 類似 Dijkstra 算法
從根結點開始 BFS, 一直保持成一顆樹
for v in V:
v.minAdjEdge = MAX
v.pre = None
root.minAdjEdge = 0
que = priority-queue (G.V) # sort by minAdjEdge
while not que.isempty():
u = que.extractMin()
for v in Adj(u):
if v in que and v.minAdjEdge>w(u,v):
v.pre = u
v.minAdjEdge = w(u,v)
- 建堆
//note it's v, not vlgv
- 主循環中
- extractMin:
- in 操作 可以另設標志位, 在常數時間完成, 總共
- 設置結點的 minAdjEdge, 需要
, 循環 E 次,則 總共
- extractMin:
綜上, 時間復雜度為
如果使用的是 斐波那契堆, 在 設置 minAdjEdge時 調用 decrease-key
, 這個操作攤還代價為 , 所以時間復雜度可改進到
4. 單源最短路
求一個結點到其他結點的最短路徑, 可以用 Bellman-ford算法, 或者 Dijkstra算法.
定義兩個結點u,v間的最短路
問題的變體
- 單目的地最短路問題: 可以將所有邊反向轉換成求單源最短路問題
- 單結點對的最短路徑
- 所有結點對最短路路徑
最短路的子路徑也是最短路徑
為從結點
到
的一條最短路徑, 對于任意
, 記
為 p 中
到
的子路徑, 則
為
到
的一條最短路徑
4.1. 負權重的邊
Dijkstra 算法不能處理, 只能用 Bellman-Ford 算法,
而且如果有負值圈, 則沒有最短路, bellman-ford算法也可以檢測出來
4.2. 初始化
def initialaize(G,s):
for v in G.V:
v.pre = None
v.distance = MAX
s.distance = 0
4.3. 松弛操作
def relax(u,v,w):
if v.distance > u.distance + w:
v.distance = u.distance + w:
v.pre = u
性質
- 三角不等式:
- 上界:
- 收斂: 對于某些結點u,v 如果s->...->u->v是圖G中的一條最短路徑,并且在對邊,進行松弛前任意時間有
則在之后的所有時間有
- 路徑松弛性質: 如果
是從源結點下v0到結點vk的一條最短路徑,并且對p中的邊所進行松弛的次序為
, 則
該性質的成立與任何其他的松弛操作無關,即使這些松弛操作是與對p上的邊所進行的松弛操作穿插進行的。
證明
4.4. 有向無環圖的單源最短路問題
def dag-shortest-path(G,s):
initialize(G,s)
for u in topo-sort(G.V):
for v in Adj(v):
relax(u,v,w(u,v))
4.5. Bellman-Ford 算法
def bellman-ford(G,s):
initialize(G,s)
for ct in range(|V|-1): # v-1 times
for u,v as edge in E:
relax(u,v,w(u,v))
for u,v as edge in E:
if v.distance > u.distance + w(u,v):
return False
return True
第一個 for 循環就是進行松弛操作, 最后結果已經存儲在 結點的distance 和 pre 屬性中了, 第二個 for 循環利用三角不等式檢查有不有負值圈.
下面是證明該算法的正確性4.6. Dijkstra 算法
, 要求不能有負值邊
Dijkstra算法既類似于廣度優先搜索(,也有點類似于計算最小生成樹的Prim算法。它與廣度優先搜索的類似點在于集合S對應的是廣度優先搜索中的黑色結點集合:正如集合S中的結點的最短路徑權重已經計算出來一樣,在廣度優先搜索中,黑色結點的正確的廣度優先距離也已經計算出來。Dijkstra算法像Prim算法的地方是,兩個算法都使用最小優先隊列來尋找給定集合(Dijkstra算法中的S集合與Prim算法中逐步增長的樹)之外的“最輕”結點,將該結點加入到集合里,并對位于集合外面的結點的權重進行相應調整。
def dijkstra(G,s):
initialize(G,s)
paths=[]
q = priority-queue(G.V) # sort by distance
while not q.empty():
u = q.extract-min()
paths.append(u)
for v in Adj(u):
relax(u,v,w(u,v))
5. 所有結點對的最短路問題
5.1. 矩陣乘法
使用動態規劃算法, 可以得到最短路徑的結構
設 為從結點i 到結點 j 的至多包含 m 條邊的任意路徑的最小權重,當m = 0, 此時i=j, 則 為0,
可以得到遞歸定義
由于對于所有 j, 有 ,所以上式后面的等式成立.
由于是簡單路徑, 則包含的邊最多為 |V|-1 條, 所以
所以可以從自底向上計算, 如下
輸入權值矩陣 ,輸出
, 其中
,
def f(L, W):
n = L.rows
L_new = new matrix(row=n ,col = n)
for i in range(n):
for j in range(n):
L_new[i][j] = MAX
for k in range(n):
L_new[i][j] = min(L_new[i][j], L[i][k]+w[k][j])
return L_new
可以看出該算法與矩陣乘法的關系
,
所以可以直接計算乘法, 每次計算一個乘積是 , 計算 V 次, 所以總體
, 使用矩陣快速冪可以將時間復雜度降低為
def f(W):
L = W
i = 1
while i<W.rows:
L = L*L
i*=2
return L
5.2. Floyd-Warshall 算法
同樣要求可以存在負權邊, 但不能有負值圈. 用動態規劃算法:
設 為 從 i 到 j 所有中間結點來自集合
的一條最短路徑的權重. 則有
而且為了找出路徑, 需要記錄前驅結點, 定義如下前驅矩陣 , 設
為 從 i 到 j 所有中間結點來自集合
的最短路徑上 j 的前驅結點
則
對
由此得出此算法
def floyd-warshall(W):
n = len(W)
D= W
initialize pre
for k in range(n):
pre2 = pre.copy()
for i in range(n):
for j in range(n)
if d[i][j] > d[i][k]+d[k][j]:
d[i][j] =d[i][k]+d[k][j]
pre2[i][j] = pre[k][j]
pre = pre2
return d,pre
5.3. Johnson 算法
思路是通過重新賦予權重, 將圖中負權邊轉換為正權,然后就可以用 dijkstra 算法(要求是正值邊)來計算一個結點到其他所有結點的, 然后對所有結點用dijkstra
- 首先構造一個新圖 G'
先將G拷貝到G', 再添加一個新結點 s, 添加 G.V條邊, s 到G中頂點的, 權賦值為 0 - 用 Bellman-Ford 算法檢查是否有負值圈, 如果沒有, 同時求出
- 求新的非負值權, w'(u,v) = w(u,v)+h(u)-h(v)
-
對所有結點在 新的權矩陣w'上 用 Dijkstra 算法
image.png
JOHNSON (G, u)
s = newNode
G' = G.copy()
G'.addNode(s)
for v in G.V: G'.addArc(s,v,w=0)
if BELLMAN-FORD(G' , w, s) ==FALSE
error "the input graph contains a negative-weight cycle"
for v in G'.V:
# computed by the bellman-ford algorithm, delta(s,v) is the shortest distance from s to v
h(v) = delta(s,v)
for edge(u,v) in G'.E:
w' = w(u,v)+h(u)-h(v)
d = matrix(n,n)
for u in G:
dijkstra(G,w',u) # compute delta' for all v in G.V
for v in G.V:
d[u][v] = delta'(u,v) + h(v)-h(u)
return d
6. 最大流
G 是弱連通嚴格有向加權圖, s為源, t 為匯, 每條邊e容量 c(e), 由此定義了網絡N(G,s,t,c(e)),
- 流函數
其中是以 v 為頭的邊集,
是以 v 為尾的邊集
- 流量:
- 截
:
- 截量
6.1. 定理[2]
- 對于任一截
, 有
prove -
證明: 由上面定理
而, 則
- 最大流,最小截: 若
, 則F'是最大流量, C(S) 是最小截量
6.2. 多個源,匯
可以新增一個總的源,一個總的匯,
6.3. Ford-Fulkerson 方法
由于其實現可以有不同的運行時間, 所以稱其為方法, 而不是算法.
思路是 循環增加流的值, 在一個關聯的"殘存網絡" 中尋找一條"增廣路徑", 然后對這些邊進行修改流量. 重復直至殘存網絡上不再存在增高路徑為止.
def ford-fulkerson(G,s,t):
initialize flow f to 0
while exists an augmenting path p in residual network Gf:
augment flow f along p
return f
6.3.1. 殘存網絡
6.3.2. 增廣路徑
6.3.3. 割
6.4. 基本的 Ford-Fulkerson算法
def ford-fulkerson(G,s,t):
for edge in G.E: edge.f = 0
while exists path p:s->t in Gf:
cf(p) = min{cf(u,v):(u,v) is in p}
for edge in p:
if edge in E:
edge.f +=cf(p)
else: reverse_edge.f -=cf(p)