算法概論筆記 - 圖

現實生活中有很大一類問題可以用簡潔明了的圖論語言來描述,可以轉化為圖論問題。


圖操作問題

相關定義

圖可以表示為G=(V, E)

  • 頂點集合V(非空但有限)
  • 連接頂點的邊的集合E(可以為空)

通常用|V|表示頂點的數量,用|E|表示邊的數量

對于頂點x,y

  • 無向邊的表示:{x, y}(對稱,無序)
  • 有向邊的表示:(x, y) 與(y, x)(有序)

有時邊具有權值(weight/ cost)

路徑

路徑的長 = 該路徑上的邊數

  • 如果路徑不包含邊,則路徑的長為0,如一個頂點到其自身
  • 環:路徑x->x長至少為1
  • 圈:對于有向圖,路徑x->x長至少為1

無圈的有向圖DAG - Directed Acyclic Graph

連通

  • 連通圖(無向圖):無向圖每個頂點到其他每個頂點都存在一條路徑
  • 強連通(有向圖):有向圖每個頂點到其他每個頂點都存在一條路徑
  • 弱連通(有向圖):有向圖的基礎圖(即其邊去掉方向所形成的的圖)是連通的
  • 完全圖:每一對頂點間都存在一條邊的圖
  • 連通部件:每個都是原圖的一個子圖,并且內部頂點間彼此相連通,但卻與部件外,即原圖中的其他頂點間沒有邊可達,如下圖的{A, B, E, I, J} {C, D, G, H, K, L} {F}
3個連通部件

  • x的入度:以頂點x為終點的邊數目
  • x的出度:以頂點x為起點的邊數目

圖的表示

1. 鄰接矩陣

用一維數組存儲圖中頂點的信息,用二維數組A表示途中各頂點之間的鄰接關系或者權值。

對于每條邊(x, y),

  • A[x, y] = 1/ weight(如果存在該條邊)
  • A[x, y] = 0/ 很大或很小的權值 (如果不存在該條邊)

優點:簡單,在常量時間內確定某條邊是否存在

缺點:空間需求O(|V|^2),適合稠密圖(E = O(|V|^2)),然而大部分應用情況并非如此

2. 鄰接表(標準方法)

對于每個頂點,使用一個表存放所有鄰接的頂點。

優點:空間需求O(|E| + |V|);若邊有權,那么附加信息也可以存儲在鄰接表中。

缺點:確定某條邊是否存在的操作需要遍歷一個頂點的鄰接表

對于非常稀疏的圖,當使用ArrayList時程序員可能需要從一個比默認容量更小的容量開始ArrayList,否則可能造成明顯的空間浪費。
鄰接表的實現:1. 鄰接表作為Vetex的映射(Map); 鄰接表作為Vetex的數據成員

3. 其他

鄰接多重表、十字鏈表法、邊集數組

深度優先搜索

基本問題:給定圖中的一個頂點,從該頂點出發,圖中那些部分是可達的?

procedure explore(G, v)
Input: G={V, E} is a graph; v in V
Output: visited(v) is set to true for all nodes u reachable from v

    visited(v) = true
    previsit(v)
    for each edge(u, v) in E:
        if not visited(u):explore(u)
    postvisit(v)

procedure dfs(G)
for all v \in V:
    visited(v) = false
for all v \in V:
    if not visited(v):explore(v)

previsit/postvisit 當一個頂點被首次訪問/最后一次訪問時對其進行的操作

每次在procedure explore中調用procedure explore一次,就能夠生成一個連通部件。若僅有一個連通部件,則為連通圖

效率:由于對已訪問頂點的維護,因此每個頂點僅調用procedure explore一次,在這其中進行了以下步驟:

  1. 一些常量時間的操作 - 標記一個頂點被訪問,以及previsit/postvisit操作
  2. 一個對鄰邊進行遍歷的循環,以確定該遍歷過程能否將整個搜索過程引向未曾訪問的區域

T = T(Step 1) + T(Step 2) = T(|V|) + T(|E|)線性時間

編號

若對頂點訪問添加時間信息:

procedure previsit(v)
pre[v] = clock;
clock = clock + 1;

procedure postvisit(v)
post[v] = clock;
clock = clock + 1;

顯然,因為棧的特征,對于任意頂點u和v,[pre[u],post[u]]和[pre[v],post[v]]兩個區間要么彼此分離,要么一個包含另一個

有向圖的深度優先搜索時間信息

對于有向圖,我們可以對上圖的邊進行分類:

  1. 樹邊:DFS樹的實際組成部分;
  2. 前向邊:DFS樹從一個頂點指向該頂點的一個非子頂點后裔的邊;
  3. 回邊:DFS樹從一個頂點指向其祖先的邊;
  4. 橫跨邊:從一個頂點指向一個已經完全訪問過(已進行過postvisit)的頂點。
邊(u, v) 邊的類型
pre(u)<pre(v)<post(v)<post(u) 樹邊/前向邊
pre(v)<pre(u)<post(u)<post(v) 回邊
pre(v)<post(v)<pre(u)<post(u) 橫跨邊

基于上述定義,可得,有向圖含有一個環當且僅當深度優先搜索過程中探測到一條回邊

無環性、可線性化以及無回邊性實際上是一回事

see also graph.dfs.DeepFirstSearch@judgeBackEdge

迷宮探索(無向圖/深度優先搜索)

從一個給定的地點開始迷宮行走,一旦到達某個關節點(相當于頂點),您將面對一些路徑(相當于邊)供您選擇。如果選擇不慎,結果可能使您不停地兜圈子,或是使您忽視迷宮中一些可行的路徑。

迷宮探索問題 轉化-> 可達性問題

關鍵點:

物品 作用 真實表現
粉筆 標記訪問過的頂點 每個頂點維護一個布爾量
細繩 將您帶回出發位置 使用棧/或者通過遞歸隱含地使用棧:延伸以到達一個新頂點;回溯到曾經訪問的頂點
方式1

利用粉筆與細繩

see also graph.ExploreMaze

方式2

假設迷宮的起始頂點和出口頂點分別標記為startend,并將無向圖視作有向圖。那么,我們可以得知路徑(u, v)是一條樹邊/前向邊,組成該條路徑的也必是若干條樹邊/前向邊。

那么可從pre[]與post[]數組中得出:

  1. pre/post[start/end]判斷是否為樹邊/前向邊 - 迷宮是否探索成功
  2. 從start和end兩端,夾逼中間的樹邊/前向邊 - 得出中間路徑。

特別地,可直接在剛找到end的時間點進行

深度優先實現: TODO

拓撲排序(有向無圈圖)

如果存在一條x到y的路徑,那么在排序中y就出現在x的后面。

聯想: 課程先修結構

方式1

基本思想:
?
1. find a vertex of zero indegree
2. assign the vertex a topo num
3. decrease the indegree of vertexes which are adjacent to the vertex.
4. jump to step 1 until the topo num runs out
step 1處潛在地查看了所有頂點,可利用隊列存儲所有入度為0的頂點進行優化。

1. work out the queue containing all the vertexes of zero indegree
2. dequeue the queue to get a vertex, and assign the vertex a topo num
3. decrease the indegree of vertexes which are adjacent to the vertex, `if the indegree decreased to be zero, than add this vertex to the queue`
4. jump to `step 2`

see also graph.TopologySort

無圈通過是否有入度為0的頂點判斷

方式2

基本思想:

簡單對圖頂點執行深度優先搜索,按照頂點的post值降序
進一步,我們可知道,post值最小的頂點一定出現在拓撲排序末尾,一定是匯點 - 出度為0。 對稱地,post值最大的一定是源點 - 入度為0。

see also graph.dfs.DfsSolveTopoSort@topoSort

無圈通過回邊判斷

廣度優先搜索(最短路徑樹)


類似于對樹的層序遍歷(level-order traversal)

procedure bfs(G, s)
Input: Graph G = {V, E}, s \in V
Output: For all vertexes u reachable form s, dist(u) is set to tht distance from s to u

for all u in V
    dist(u) = \infty

dist(s) = 0
Q = [s](queue containing just s)
while Q is not empty
    u = eject(Q)
    for all edges (u, v) in E
        dist(v) = \infty:
            inject(Q, v)
            dist(v) = dist(u) + 1;

T = 2|V|次隊列操作 + 訪問邊 = O(|V| + |E|)

局限性:邊均為單位長度

see also graph.bfs.BreadthFirstSearch

單源最短路徑

當前,還不存在找出從s到一個頂點的路徑比找出從s到所有頂點路徑更快(快得超出一個常數因子)的算法

邊為正整數
引入虛頂點

通過在G中的邊引入“虛”頂點,分解為單位長度的小段。此時可以通過運行bfs計算最短路徑,然而存在效率問題。

對感興趣的頂點分別設置鬧鐘(該頂點的預計到達時間)。事實上,隨著廣度優先搜索的進行,會發現一些捷徑,相應頂點的預計到達時間會發生松弛

- s設定的鬧鐘為時刻0,從s到u的距離是T
- 對于G中u的每個鄰居v:
    1. 如果尚未給v設定鬧鐘,則為其設定鬧鐘時刻為T+l(u, v)
    2. 如果v的鬧鐘時刻設定比T+l(u, v)要晚,則將它設定為T+l(u, v)
Dijkstra算法

上述思想即可自然引出Dijkstra算法,該算法可用于解決單源無負邊最短路徑問題。

數據結構:優先隊列,并支持以下操作

  1. 插入新的元素
  2. 減少某個特定元素的鍵值
  3. 返回并刪除鍵值最小的元素
  4. 用給定的元素以及給定的元素鍵值構建一個隊列

額外數組prev

存儲該頂點之前的頂點
通過這些后向指針,從而方便地重建所需的最短路徑

procedure dijkstra(G, l, s)
Input: Graph G=(V, E), s \in V)
    positive edge lengths{l_e:e \in E}
Output: For all vertexes u reachable form s, dist(u) is set to tht distance from s to u

for all u \in V
    dist(u) = \infty
    pre(u) = nil

dist(s) = 0
PQ = makequeue(V) (using dist-values as keys)
while PQ is not empty
    u = deletemin(PQ)
    for all edges (u, v) \in E
        if dist(v) > dist(u) + l(u, v)
            dist(v) = dist(u) + l(u, v)
            pre(v) = u
            decreasekey(PQ, v)

Notice: 因初始時pre(u) = nil,因此makequeue時不能搶占該邊界次數(即直接將與s相鄰的頂點直接加入)。
T = |V|次插入操作 + |V|次deletemin操作 + |E|次decreasekey操作
T嚴重依賴于優先隊列所用的實現方法,例如數組、二分堆、d堆、Fibonacci堆。

若采用數組實現優先隊列,則T = O(|V|^2 + E)
see also 數組實現graph.bfs.DijkstraDfsWithArray

若采用二分堆實現優先隊列,則T = O((|V|+|E|)log|V|)

堆的高度為log|V|

see also 二分堆實現graph.bfs.DijistraDfsWithHeap
二分堆由最小堆實現,decreasekey操作首先需要找到v在二分堆中的位置。考慮到存在value
相同的元素,則找位置需要耗費O(|V|)復雜度,則將使得T從原來的O((|V|+|E|)log|V|)降為O(|V|log|V|+|V||E|)

  1. 一種好的做法是
    記錄(v, dist(v))在堆中的位置,并在優先隊列改變時更新

可以考慮使用配對堆(pairing heap)

  1. 另一種方法是在每次dist(v)變化時把(v, dist(v))插入到優先隊列中去。這樣,在優先隊列中的每個頂點可能有多于一個的代表。當deletemin操作把最小的頂點從優先隊列中刪除時,必須檢查以肯定它不是known的。如果它是,則忽略它,并執行另一次deletemin。

因隊列的大小可能達到|E|+|V|。由于|E|<=|V|^2, log|E|<=2log|V|。另外,可能需要|E|次而不是|V|次deletemin操作。
T = (|V|+|E|)*log(|E|+|V|) = O((|V|+|E|)log|V|)。時間減慢,時間復雜度不變。

Bellman-Ford算法(可處理負邊、可檢測負環)

Dijkstra按照正確的順序更新所有邊。對于標記過的點不在進行更新,即使有負權導致最短路徑的改變也不會進行重新計算。

而Bellman-Ford算法更新所有邊,每條邊更新|V|-1次.

考慮到對每條邊進行第1次松弛操作的時候,實際上是考慮至多經過(不考慮源點終點)0個點的路徑;對每條邊進行第2次松弛操作的時候,實際上是考慮至多經過1個點的路徑...而對于無負邊的圖而言,最短路徑至多經過n-2個點,因此在|V|-1次迭代后,可以得到最短路徑

procedure BellmanFord(G, l, s)
Input: Graph G=(V, E), s \in V
    edge lengths{l_e:e \in E}
Output: For all vertexes u reachable form s, dist(u) is set to tht distance from s to u

for all u \in V
    dist(u) = \infty
    pre(u) = nil

dist(s) = 0
repeat |V|-1 times:
    for all e(u, v) \in E:
        if(dist(v) > dist(u) + e(u,v))
            dist(v) = dist(u) + e(u,v)
            pre(v) = u

可檢測負環

  • 在|V|-1次迭代后再執行一次迭代過程。若這最后一次迭代中有某個dist的值被減少,那么圖中存在一個負環。
  • 圖中出現負環,最短路徑問題本身就沒有意義

T = O(|V||E|)
優化

  • 若在該輪,所有的邊都沒有relax的話,算法便可結束。因此有時候無需經過|V|-1次迭代
  • 每次僅對最短路徑估計值發生變化了的頂點的所有出邊執行松弛操作

可用于解m個n元約束組,即相當于n個頂點m條邊。

復雜度為O(m*n)

TODO 差分約束系統和SPAF

基于拓撲排序的最短路徑(有向無圈圖)

通過深度優先搜索線性化有向無環圖,然后按照得到的頂點順序訪問頂點,對從當前訪問頂點出發的邊執行更新操作。

procedure dag-shortest-paths(G, l, s)
Input: Graph G=(V, E), s \in V
    edge lengths{l_e:e \in E}
Output: For all vertexes u reachable form s, dist(u) is set to tht distance from s to u

for all u \in V
    dist(u) = \infty
    pre(u) = nil

dist(s) = 0
Linearize G
for each u \in V in linearized order:
    for all edges (u, v) \in E:
        if(dist(v) > dist(u) + e(u,v))
            dist(v) = dist(u) + e(u,v)
            pre(v) = u

當一個頂點v被選取以后,按照拓撲排序的法則它沒有從unknown頂點發出的進入邊,因此它的距離dist(v)不再被降低。

T = O(|V| + |E|)

see also graph.TopoSortSolveShortPath

單源最長路徑(有向無圈圖)

對于一般的圖,最長路徑問題通常沒有意義,因為可能有正值圈(等價于最短路徑問題中的負值圈)存在。對于DAG而言,將每條邊的長度設為其負值,使用前述的拓撲排序方法,即可得到單源最長路徑。

see also graph.TopoSortSolveLongPath

應用

事件節點圖

  • 最早完成時間:從節點1開始,按照拓撲順序,計算單源最長路徑
  • 最晚完成時間:從節點n開始,按照反序的拓撲順序,計算單源最短路徑

網絡流問題

給定有向圖G=(V, E),其邊容量為e(u, v)。

容量代表一個管道允許通過的水的最大流量。

有兩個頂點,發點s和收點t,最大流問題就是確定從s到t可以通過的最大流量。

必須滿足流守恒:進入的都必然流出

方法:構造構造流圖(算法終止時包含最大流)、殘余圖(表示對于每條邊的剩余流容量)。尋找增長通路,構造反向弧。當沒有增長通路時,算法終止。

不需要保證是無圈圖

增長通路:殘余圖中從s到t的一條路徑。該路徑上的最小邊值就是可以添加到路徑每一邊上的流的量。

反向弧:對于流圖中具有流f(u, v)的每一邊(u, v),我們在殘余圖中添加一條容量為f(u, v)的反向邊(v, u)。

使算法可以解除/部分解除它原來的決定,使得可以找到最優解

通過bfs尋找增廣路徑,T = O(|E|^2*|V|)
see also graph.bfs.NetWorkFlowEk
see more

參考文獻

1. bellman-ford算法與差分約束系統

寫在最后

  • 立個Flag,TODO will be done some day。
  • 渣代碼,且輕噴?:worried:?。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,106評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,441評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,211評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,736評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,475評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,834評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,829評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,009評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,559評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,306評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,516評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,038評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,728評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,132評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,443評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,249評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,484評論 2 379

推薦閱讀更多精彩內容

  • https://zh.visualgo.net/graphds 淺談圖形結構https://zh.visualgo...
    狼之獨步閱讀 4,186評論 0 0
  • 第一章 緒論 什么是數據結構? 數據結構的定義:數據結構是相互之間存在一種或多種特定關系的數據元素的集合。 第二章...
    SeanCheney閱讀 5,807評論 0 19
  • 圖是一種比線性表和樹更復雜的數據結構,在圖中,結點之間的關系是任意的,任意兩個數據元素之間都可能相關。圖是一種多對...
    Alent閱讀 2,330評論 1 22
  • -DFS(Depth First Search):深度優先搜索 訪問完一個頂點的所有鄰接點之后,會按原路返回,對應...
    Spicy_Crayfish閱讀 2,850評論 1 0
  • 第二天我拖著病軀,和萬熙然一起將出租屋里的生活用品轉移到了陳梵給我的房子里。屋子一看就是小可愛連夜布置了一遍的,窗...
    和這個世界溫柔的相處閱讀 275評論 1 6