圖算法 1. 圖 2. 搜索--求圖的生成樹[1] 3. 最小生成樹 4. 單源最短路 5. 所有結點對的最短路問題 6. 最大流 7. 參考資料

1. 圖

1.1. 概念

  • 頂點的度 d
  • 相鄰
  • 重邊
  • 完全圖: 所有頂都相鄰
  • 二分圖: V(G) = X \cup Y, X\cap Y = \varnothing, X中, Y 中任兩頂不相鄰
  • 軌道

1.1.1. 性質

  • \sum_{v\in V} d(v) = 2|E|
  • G是二分圖 \Leftrightarrow G無奇圈
  • 樹是無圈連通圖
  • 樹中, |E| = |V| -1

1.2. 圖的表示

  • 鄰接矩陣
  • 鄰接鏈表


1.3. 樹

無圈連通圖, E = V-1, 詳細見,

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)

時間複雜度 O(V+E)

2.2. DFS

\Theta(V+E)

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 的性質

  • 其生成的前驅子圖G_{pre} 形成一個由多棵樹構成的森林, 這是因爲其與 dfsVisit 的遞歸調用樹相對應
  • 括號化結構


  • 括號化定理:
    考察兩個結點的發現時間與結束時間的區間 [u,begin,u.end] 與 [v.begin,v.end]
    • 如果兩者沒有交集, 則兩個結點在兩個不同的子樹上(遞歸樹)
    • 如果 u 的區間包含在 v 的區間, 則 u 是v 的後代

2.3. 拓撲排序

利用 DFS, 結點的完成時間的逆序就是拓撲排序

同一個圖可能有不同的拓撲排序

2.4. 強連通分量

在有向圖中, 強連通分量中的結點互達
定義 GrevG 中所有邊反向後的圖

將圖分解成強連通分量的算法
在 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 的時間接近常數
所以時間複雜度在於排序邊, 即 O(ElgE), 而 E<V^2, 所以 lgE = O(lgV), 時間複雜度爲 O(ElgV)

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)
  • 建堆 O(V) //note it's v, not vlgv
  • 主循環中
    • extractMin: O(VlgV)
    • in 操作 可以另設標誌位, 在常數時間完成, 總共 O(E)
    • 設置結點的 minAdjEdge, 需要O(lgv), 循環 E 次,則 總共O(ElgV)

綜上, 時間複雜度爲O(ElgV)
如果使用的是 斐波那契堆, 在 設置 minAdjEdge時 調用 decrease-key, 這個操作攤還代價爲 O(1), 所以時間複雜度可改進到 O(E+VlgV)

4. 單源最短路

求一個結點到其他結點的最短路徑, 可以用 Bellman-ford算法, 或者 Dijkstra算法.
定義兩個結點u,v間的最短路
\delta(u,v) = \begin{cases} \min(w(path)),\quad u\xrightarrow{path} v\\ \infty, \quad u\nrightarrow v \end{cases}
問題的變體

  • 單目的地最短路問題: 可以將所有邊反向轉換成求單源最短路問題
  • 單結點對的最短路徑
  • 所有結點對最短路路徑

最短路的子路徑也是最短路徑

p=(v_0,v_1,\ldots,v_k)爲從結點v_0v_k的一條最短路徑, 對於任意0\le i\le j \le k, 記p_{ij}=(v_i,v_{i+1},\ldots,v_j)爲 p 中 v_iv_j的子路徑, 則 p_{ij}v_iv_j的一條最短路徑

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

性質

  • 三角不等式: \delta(s,v) \leqslant \delta(s,u) + w(u,v)
  • 上界: v.distance \geqslant \delta(s,v)
  • 收斂: 對於某些結點u,v 如果s->...->u->v是圖G中的一條最短路徑,並且在對邊,進行鬆弛前任意時間有 u.distance=\delta(s,u)則在之後的所有時間有 v.distance=\delta(s,v)
  • 路徑鬆弛性質: 如果p=v_0 v_1 \ldots v_k是從源結點下v0到結點vk的一條最短路徑,並且對p中的邊所進行鬆弛的次序爲(v_0,v_1),(v_1,v_2), \ldots ,(v_{k-1},v_k), 則 v_k.distance = \delta(s,v_k)
    該性質的成立與任何其他的鬆弛操作無關,即使這些鬆弛操作是與對p上的邊所進行的鬆弛操作穿插進行的。

證明


4.4. 有向無環圖的單源最短路問題

\Theta(V+E)

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 算法

O(VE)

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 算法

O(ElogV), 要求不能有負值邊

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. 矩陣乘法

使用動態規劃算法, 可以得到最短路徑的結構
l_{ij}^{(m)}爲從結點i 到結點 j 的至多包含 m 條邊的任意路徑的最小權重,當m = 0, 此時i=j, 則 爲0,
可以得到遞歸定義
l_{ij}^{(m)} =\min( l_{ij}^{(m-1)}, \min_{1\leqslant k\leqslant n}( l_{ik}^{(m-1)}+w_{kj})) = \min_{1\leqslant k\leqslant n}( l_{ik}^{(m-1)}+w_{kj}))
由於對於所有 j, 有 w_{jj}=0,所以上式後面的等式成立.

由於是簡單路徑, 則包含的邊最多爲 |V|-1 條, 所以
\delta(i,j) = l_{ij}^{(|V|-1)} = l_{ij}^{(|V|)} =l_{ij}^{(|V| + 1)}= ...
所以可以從自底向上計算, 如下
輸入權值矩陣 W(w_{ij})), L^{(m-1)},輸出L^{(m)}, 其中 L^{(1)} = W,

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

可以看出該算法與矩陣乘法的關係
L^{(m)} = W^m,
所以可以直接計算乘法, 每次計算一個乘積是 O(V^3), 計算 V 次, 所以總體 O(V^4), 使用矩陣快速冪可以將時間複雜度降低爲O(V^3lgV)

def f(W):
    L = W
    i = 1
    while i<W.rows:
        L = L*L
        i*=2
    return L

5.2. Floyd-Warshall 算法

同樣要求可以存在負權邊, 但不能有負值圈. 用動態規劃算法:
d_{ij}^{(k)} 爲 從 i 到 j 所有中間結點來自集合 {\{1,2,\ldots,k\}} 的一條最短路徑的權重. 則有
d_{ij}^{(k)} = \begin{cases} w_{ij},\quad k=0\\ min(d_{ij}^{(k-1)},d_{ik}^{(k-1)}+d_{kj}^{(k-1)}),\quad k\geqslant 1 \end{cases}
而且爲了找出路徑, 需要記錄前驅結點, 定義如下前驅矩陣 \Pi, 設 \pi_{ij}^{(k)} 爲 從 i 到 j 所有中間結點來自集合 {\{1,2,\ldots,k\}} 的最短路徑上 j 的前驅結點

\pi_{ij}^{(0)} = \begin{cases} nil,\quad i=j \ or \ w_{ij}=\infty \\ i, \quad i\neq j\ and \ w_{ij}<\infty \end{cases}
k\geqslant 1
\pi_{ij}^{(k)} = \begin{cases} \pi_{ij}^{(k-1)} ,\quad d_{ij}^{(k-1)}\leqslant d_{ik}^{(k-1)}+d_{kj}^{(k-1)}\\ \pi_{kj}^{(k-1)} ,\quad otherwise \end{cases}

由此得出此算法

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

  1. 首先構造一個新圖 G'
    先將G拷貝到G', 再添加一個新結點 s, 添加 G.V條邊, s 到G中頂點的, 權賦值爲 0
  2. 用 Bellman-Ford 算法檢查是否有負值圈, 如果沒有, 同時求出 \delta(s,v) 記爲 h(v)
  3. 求新的非負值權, w'(u,v) = w(u,v)+h(u)-h(v)
  4. 對所有結點在 新的權矩陣w'上 用 Dijkstra 算法


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)),

  • 流函數 f(e):E \rightarrow R
    \begin{aligned} (1)\quad & 0\leqslant f(e) \leqslant c(e),\quad e \in E\\ (2)\quad & \sum_{e\in \alpha(v)} f(e)= \sum_{e\in \beta(v)}f(e),\quad v \in V-\{s,t\} \end{aligned}
    其中 \alpha(v) 是以 v 爲頭的邊集, \beta(v)是以 v 爲尾的邊集
  • 流量: F = \sum_{e\in \alpha(t)} f(e)- \sum_{e\in -\beta(t)}f(e),
  • (S,\overline S): S\subset V,s\in S, t\in \overline S =V-S
  • 截量C(S) = \sum_{e\in(S,\overline S)}c(e)

6.1. 定理[2]

  • 對於任一截(S,\overline S), 有 F = \sum_{e\in (S,\overline S)} f(e)- \sum_{e\in(\overline S,S)}f(e),
  • F\leqslant C(S)
    證明: 由上面定理
    F = \sum_{e\in (S,\overline S)} f(e)- \sum_{e\in(\overline S,S)}f(e),
    0\leqslant f(e) \leqslant c(e), 則
    F\leqslant \sum_{e\in (S,\overline S)} f(e) \leqslant \sum_{e\in (S,\overline S)} c(e) = C(S)
  • 最大流,最小截: 若F= C(S), 則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)

6.5. TBD

7. 參考資料


  1. 算法導論

  2. 圖論, 王樹禾

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章