1. 程式人生 > >演算法導論-第六部分-讀書筆記

演算法導論-第六部分-讀書筆記

第二十一章 用於不相交集合的資料結構

第二十一章本來是第五部分裡的,但它的內容和第六部分關係更為密切,所以放到了這裡。

21.1 不相交集合的操作

不相交集合資料結構(disjoint-set data structure):維護了一個不相交可變集的集合 S={S1, S2, …, Sk},S1…Sk 都是不相交的。

在不相交可變集合中,我們選出一個元素代表這個集合,用來在判斷集合是否相交時做判斷用,這個元素叫做:代表(representative)。例如:x、y、z 在一個集合裡,z 是這個集合的代表,在判斷 x 和 y 是否相交時,看看 x 和 y 的代表是不是相同,因為他們的代表都是 z,所以它們是相交的。

  • MAKE-SET(X):建立一個新的集合,它的唯一成員(因而是代表)是 x。因為各個集合不是相交的,故 x 不會出現在別的某個集合中。
  • UNION(X, Y):將包含 x 和 y 的兩個動態集合(表示為 Sx 和 Sy)合併成一個新的集合,即這兩個集合的並集。
  • FIND-SET(X):返回一個指標,這個指標指向包含 x 的(唯一)集合的代表。(返回代表來判斷兩個元素是否是相交的)

不相交集合資料結構的一個應用

不相交集合資料結構的許多應用之一是,確定無向圖的連通分量。例如,下圖包含了 4 個連通分量的圖。
程式碼中,圖 G 的頂點用 G.V 表示,邊集用 G.E 表示。
這裡寫圖片描述

21.2 不相交集合的連結串列表示

下圖給出了實現不相交資料結構的簡單方法:每個集合用一個連結串列來表示。

  • 每個集合物件包含 head 和 tail 屬性,head 指向第一個物件,tail 指向連結串列的最後一個物件。
  • 連結串列中每個物件都包含 3 個屬性:
    • 一個集合成員(就是關鍵字的意思)
    • 一個指向下一個物件的指標
    • 一個指回集合物件的指標

這裡寫圖片描述

關於合併的一個簡單實現

UNION 操作比 MAKE-SET 或 FIND-SET 花的時間多。因為就像上面的圖一樣,在 UNION(x, y)時是把 y 加到 x 裡面,除了修改 tail 指標和 x 裡最後一個元素的指標,還要修改 y 裡所有元素的指向集合的指標。

如果我們要把 n 個物件,建立集合並進行 UNION 操作的話,最慢的方法需要 O(n2)時間。
如下圖所示,建立時候,更新物件數為1。但在 UNION 時候,更新物件數是線性增長的(1…n-1),因為每次進行 UNION 時候,都是把大的集合向小的集合進行合併,這樣要修改大的集合的每個元素的指標(是指向集合的那個指標)。這樣每個操作平均需要 Θ(n) 時間,也就是說,一個操作的攤還時間為 Θ(n)

如果是把小的集合向大的集合上合併的話,就會快很多,這也是下面要講的加權合併。
這裡寫圖片描述

一種加權合併的啟發式策略

把較短的表拼接到較長的表中,叫做加權合併啟發策略。

21.3 不相交集合森林

在一個不相交集合森林中,每個成員僅指向它的父結點。每棵樹的根包含集合的代表,並且是其自己的父結點。正如我們將要看到的那樣,雖然使用這種表示的直接演算法並不比使用連結串列表示的演算法快,但通過引入兩種啟發式策略(按秩合併 和 路徑壓縮),我們能得到一個漸近最優的不相交集合資料結構。
這裡寫圖片描述

改進執行時間的啟發式策略

按秩合併(union by rank)

它類似於連結串列表示中使用的加權合併啟發式策略。顯而易見的做法是,使具有較少結點的樹的根指向具有較多結點的樹的根。這裡並不顯式地記錄每個結點為根的子樹的大小,而是採用一種易於分析的方法。對於每個結點,維護一個秩,它表示該結點的高度的一個上界。在使用按秩合併策略的 UNION 操作中,我們可以讓具有較小秩的根指向具有較大秩的根。

路徑壓縮

在 FIND-SET 操作中,使用這種策略可以使查詢路徑中的每個結點直接指向根。路徑壓縮並不改變任何結點的秩。
這裡寫圖片描述

虛擬碼實現

下面的虛擬碼使用了上面兩種啟發式策略:

  • UNION:使用了“按秩合併”策略
  • FIND-SET:使用了“壓縮路徑”

FIND-SET程式碼如下:
這裡寫圖片描述
這個程式碼很有意思,分成兩個部分分析:

  • 遞迴到根結點:這個過程是找到根結點
  • 找到根結點後的返回:最終結果,是把根結點返回給 FIND-SET 呼叫都。但這個過程中,把經過的所有結點的 p 指標都指向了根結點(x.p = FIND-SET(x.p))。

UNION 和 MAKE-SET 程式碼如下:

這裡寫圖片描述

這裡寫圖片描述

union 操作會形成一棵線性樹,Find 操作會把這棵線樹變平

啟發式策略對執行時間的影響

如果單獨使用按秩合併或路徑壓縮,他們每一個都能發病不相交森林上操作的執行時間,而一起使用這兩種啟動式策略時,這種改善更大。具體時間複雜度省略。

第二十二章 基本圖演算法

本章主要講”圖的表示”和”圖的搜尋”。

1,什麼是圖的搜尋?
是系統化地跟隨圖中的邊,來訪問圖中的每個結點。

2,關於圖演算法
許多的圖演算法在一開始都會先通過搜尋來獲得圖的結構,其它的一圖演算法則是對基本的搜尋加以優。圖搜尋技巧是圖演算法領域的核心。

22.1 圖的表示

圖的表示有兩種方式:

  • 鄰接連結串列:在表示稀疏圖(邊的條數 |E| 遠遠小於 |V|2 的圖)時裡非常緊湊而成為通常的選擇。
  • 鄰接矩陣:在稠密圖(|E| 接近 |V|2 的圖)情況下,我們可能傾向於使用鄰接矩陣表示法。另外,需要快速判斷任意兩個結點之間是否有邊相連,可能也需要使用鄰接矩陣表示法( O(1) 就可以計算出來,連結串列法的話,需要遍歷連結串列中每個結點)。

下看面的圖就知道了,當 |E| 遠遠小於 |V|2 時,矩陣裡的大部分都是 0,佔用了很多空間但表示出有意義的內容(也就是1)非常少。這種情況下,把是 1 的部分使用連結串列來儲存,裡非常節省空間。

這裡寫圖片描述

下面說幾個後面會用到的定義:

  • 對於圖 G=(V, E) 來說,其鄰接連結串列表示一個包含 |V| 條連結串列的陣列 Adj 所構成,每個結點有一條連結串列。對於每個結點 u∈ V,鄰接連結串列 Adj[u] 包含所有與結點 u 之間有邊相連的結點 v,即 Adj[u] 包含圖 G 中所有與 u鄰接的結點(或指標)。例如:上圖 (a) 中的 12345 每個結點都是 Adj 陣列中的一個元素,結點 1 與 2 和 5 結點相連,所以 Adj[1] 這個元素所代表連結串列中,有 2 和 5 兩個元素。(在虛擬碼中,我們將陣列 Adj 看成圖的一個屬性,用 G.Adj[u] 這樣的表示。)
  • 如果 G 是一個“有向圖”,則對於邊 (u, v) 來說,結點 v 將出現在連結串列 Adj[u] 裡。因此,所有鄰接連結串列的長度之和等於 |E|。如果 G 是一個無向圖,對於邊 (u, v) 來說,結點 v 將出現在連結串列 Adj[u]裡,結點 u 也將出現在連結串列 Adj[v] 裡,因此所有鄰接連結串列的長度之和等於 2*|E|。

    有向圖的邊 (u, v) 表示,從頂點 u 指向 頂點 v)

  • 無論是有向圖還是無向圖,鄰接連結串列表示法的儲存空間需要均為 Θ(V+E),這正是我們所希望的數量級。

  • 對鄰接連結串列稍加修改,就可以用來表示權重釁。權重圖是圖中每條邊都帶有一個相關的權重的圖。該權重值由一個 w: E -> R 的權重函式給出。例如:設 G=(V, E) 為一個權重圖,其權重函式為 w,我們可以直接將邊 (u, v) ∈ E 的權重值 w(u, v) 存放在結點 u的鄰接連結串列中。鄰接連結串列表示法的健壯性很高,可以對其進行簡單修改來支援許多其它的圖的變種。
  • 鄰接連結串列的一個潛在缺陷是:無法快速判斷一條邊 (u, v) 是否是圖中的一條邊,唯一的辦法是在鄰接連結串列 Adj[u] 裡面搜尋結點 v。鄰接矩陣克服這個缺陷,但付出的代價是更大的儲存空間(儲存空間的漸近數量級更大)。
    這裡寫圖片描述
  • 從上面的 圖(c) 中可以看出,無向圖的鄰接矩陣是一個對稱矩陣。由於在無向圖中,邊 (u, v) 和 (v, u) 是同一條邊,無向圖的鄰接矩陣 A 就是自己的轉置,即 A=AT。在某些應用中,可能只需要存放對角線及其以上的這部分(即半個矩陣),空間需要減少一半。
  • 鄰接矩陣也可以用來表示權重圖。原來矩陣每個元素放 0 或 1,可以改成放權重值或函式。當不存在時,可以使用 NIL。但對於許多問題來說,可以用 0 或 無窮大 來表示可能更方便。
  • 雖然鄰接連結串列和鄰接矩陣在漸近意義下至少是一樣空間有效的,但鄰接矩陣表示法更為簡單,因此在圖規模比較小時,傾向於使用鄰接矩陣表示法。而且對於無向圖來說,鄰接矩陣還有一個優勢:每個記錄項只需要1位的空間。

22.2 廣度優先搜尋

廣度優先搜尋是最簡單的圖搜尋演算法之一,也是許多重要的圖演算法的原型。Prim 的最小生成樹演算法 和 Dijkstra 的單源最短路徑演算法 都使用了類似廣度優先搜尋的思想。

1,什麼是廣度優先搜尋
給定圖 G=(V, E) 和一個結點 s,通過對圖 G中的“邊”進行探索,可以發現從結點 s 到達所有結點路徑。
在搜尋時,先對結點 s 能到達的所有“鄰接連結串列中的結點”(就是結點 s 連結串列裡的結點)進行操作,再對鄰接連結串列中的結點能夠到達的結點再進行操作。。。例如,下圖中,先結 s 結點的鄰接連結串列中的結點“w, r”進行操作,然後再對結點“t, w”(w的鄰接連結串列中的結點) 和 結點“v”(r的鄰接連結串列中的結點)進行操作。。。

該演算法能夠計算從結點 s 到每個可到達的結點的距離(最小的邊數),同時生成一棵“廣度優先搜尋樹”。該樹以結點 s 為根結點,包含所有可以從 s 到達的結點。對於每個從結點 s 可以到達的結點 v,搜尋樹裡的從 s 到 v 的簡單路徑,就是圖 G 中從 s 到 v 的“最短路徑”。這就是樹的作用。該演算法可用於“有向圖”和“無向圖”。

這裡寫圖片描述

2,具體演算法內容
在搜尋時,先對結點 s 能到達的所有“鄰接連結串列中的結點”(就是結點 s 連結串列裡的結點)進行操作,再對鄰接連結串列中的結點能夠到達的結點再進行操作。。。例如,下圖中,先結 s 結點的鄰接連結串列中的結點“w, r”進行操作,然後再對結點“t, w”(w的鄰接連結串列中的結點) 和 結點“v”(r的鄰接連結串列中的結點)進行操作。。。

在搜尋過程中,結點可以有3種顏色“白,灰,黑”。最開始所有結點都是白色;在搜尋過程中,結點第一次被找到,結點變成灰色。當一個結點的鄰接連結串列中的結點都被找到(也就是變成灰色)後,這個結點變成黑色。例如,上圖中,最開始找的是提供出來的結點 s,s 被找到後變成灰色。色後找 s 的鄰接連結串列中的結點 w、r,被找到後 w、r 變成灰色,s 變成黑色。。。當一個灰色結點或黑色結點被重複找到後,顏色不變。

程式如下:
這裡寫圖片描述
注意:

  • 佇列 Q 是一個普通的先進先出的佇列
  • Q裡包含的都是灰色的結點

3,分析
總執行時間為 O(V+E)

4,廣度優先樹
下面的虛擬碼將打印出從結點 s 到 v 的一條最短路徑上的所有結點,這裡假定 BFS 已經計算出一棵廣度優先樹。

程式有點像二叉樹搜尋的程式,先遞迴找到結點 s,再在回去路上打印出所有結點。
這裡寫圖片描述

22.3 深度優先搜尋

深度優先搜尋總是對最近才發現的結點 v 的出發邊進行探索,直到該結點的所有出發邊都被發現為止。一旦結點 v 的所有出發邊都被發現,搜尋則“回溯”到 v 的前驅結點(v 是經過該結點才被找到的),來搜尋該前驅結點的出發邊。一直到最開始結點可以到達的所有結點都被找到。如果還有沒有被找到的結點,則從沒有被找到的結點中隨便找一個,再進行同樣的搜尋。直到所有結點都被找到為止。

在討論廣度優先搜尋演算法時,我們將源結點(最開始的那個結點)的數量限制為一個,而深度優先搜尋則可以有多個源結點。雖然從概念上看,廣度優先搜尋可以從多個源結點開始搜尋,深度優先搜尋也可以限制為從一個源結點開始,但本書所採取的方法所反映的是這些搜尋結果是如何被使用的。廣度優先搜尋通常用來尋找從特定源結點出發的最短路徑距離(及其相關的前驅子圖),而深度優先搜尋則沉澱作為另一個演算法裡的一個子程式,我們將在本章後面看到這點。

每當發現一個結點 v 時,深度優先搜尋演算法將這個事件進行記錄,將 v 的前驅屬性 x.π 設定為 u。不過與廣度優先不同的是,廣度優先搜尋的前驅子圖形成一棵樹,而深度優先搜尋的前驅子圖可能由多棵樹組成,因為搜尋可能從多個源結點重複進行。例如,下面的圖中,需要從 u 和 w 兩個源結點進行搜尋,才能搜尋到所有的結點,這樣就會有兩棵樹,而前驅子樹也會有兩棵。

深度優先搜尋中,結點也是有顏色的,意義和廣度優先一樣。這樣的方法可以保證每個結點僅在一棵嘗試優先樹中發現。因此,所有嘗試優先樹是不相交的。

嘗試優先演算法還在每個結點上加一個時間戳。每個結點有兩個時間戳:

  • 第一個時間戳 v.d 記錄 v 第一次被找到的時間(塗上灰色的時候)
  • 第二個時間戳 v.f 記錄的是“v 的鄰接連結串列都掃描完成”的時間

結點 u 在時刻 u.d 之前為白色,在時間 u.d 和 u.f 之間為灰色,在時間 u.f 之後為黑色。

圖如下:
這裡寫圖片描述

程式如下:
這裡寫圖片描述

注意:

  • time 是一個全域性變數
  • 深度優先演算法的結果,可能依賴於下面兩點:(不過這些不同的訪問次序在實際中不會導致問題,因為我們沉勇可以對任意的深度優先搜尋結果加以有效的利用,並獲得等價的結果)
    • 演算法 DFS 第5行對結點進行檢查的順序
    • 演算法 DFS-VISIT 第4行對一個結點的鄰接結點進行訪問的次序

分析

深度優先搜尋的執行時間為:Θ(V+E)

深度優先搜尋的性質

1,其生成的前驅子圖 Gπ 形成一個由多棵樹所構成的森林,這是因為深度優先樹的結構與 DFS-VISIT 的遞迴呼叫結構完全對應。

什麼是前驅子圖?個人理解的意思就是,生成的樹。廣度優先生成一棵,深度優先成生多棵。

2,結點的發現時間和完成時間具有所謂的括號化結構(parenthesis structure)。如果以左括號 (u 來表示結點 u 的發現,以右括號 u) 來表示結點 u 的完成,則發現和完成的歷史記錄會形成一個規整的表示式,即所有括號都適當地巢狀在一起。如下圖 (b)
這裡寫圖片描述

邊的分類

深度優先搜尋的另一個有趣的性質是,可以通過搜尋來對輸入圖 G=(V, E) 的邊進行分類。每條邊的型別可以提供關於圖的重要資訊。例如,下一節我們將看到有向圖是無環圖,當且僅當深度優先搜尋不產生“後向”邊。

  1. 樹邊:是深度優先森林 Gπ 中的邊。如果結點 v 是因為演算法對邊 (u, v) 的探索而首先被發現的,則 (u, v) 是一條樹邊。

  2. 後向邊:是對於 邊 (u, v) 來說,將結點 u 連線到其所在的深度優先樹中的一個祖先結點 v 的邊。簡單來說,v 是 u 的一直向上的祖先結點。如果 v 是 u 的祖先結點的其它子結點,就算 v 比 u 高,也不是後向邊,那是橫向邊。自迴圈的邊也被認識是後向邊。

    例如,x 到 z 是後向邊。但假設如果 x 到 w 也有一條線的話,這條邊不是後向邊,是橫向邊。

  3. 前向邊:是將結點 u 連線到其所在的深度優先樹中一個後代結點 v 的邊 (u, v)。如果後臺結點 v 是一真向下的後代結點的兄弟結點的話,那不是前向邊,是橫向邊。

    例如,s 到 w 是前向邊。但 w 到 x 不是前向邊。

  4. 橫向邊:指其他所有的邊。這些邊可以連線同一棵深度優先樹中的結點,只要其中一個結點不是另一個結點的祖先,也可以連線不同深度優先樹中的兩個結點。

    例如。v 到 s/w 都是橫向邊。u 到 v 是橫向邊。

遇到某些邊時,DFS有足夠的資訊來對這些邊進行分類。這裡的關鍵是,當第一次探索邊 (u, v) 時,結點 v 的顏色能夠告訴我們關於該條邊的一些資訊。

  1. 結點 v 為白色時,表明 邊(u, v) 是一條樹邊。
  2. 結點 v 為灰色時,表明 邊(u, v) 是一條後向邊。
  3. 結點 v 為黑色時,表明 邊(u, v) 是一條前向邊 或 橫向邊。

這些資訊,看一些圖的變化過程就能看出來。

第二十三章 最小生成樹

在電子電路設計中,我們沉澱老闆娘多個元件的針腳連在一起。要連線 n 個針腳,我們可以使用 n-1 根連線,每根連線連線兩個針腳。但我們希望使用的連線長度最短的那種方法。

上述佈線問題,可以用一個連通無向圖 G=(V, E) 來表示。這裡

  • V 是針腳的集合
  • E 是針腳之間的可能連線
  • 並且對於每條邊 (u, v) ∈ E
  • 權重 w(u, v) 作為連結針腳 u 和 v 的代價(也就是連線長度)

我們希望得取一個無環子集 T ⊆ E,既能夠把所有的針腳連線起來,又具有最小權重,即 w(T)=(u,v)Tw(u,v)。由於 T 是無環的,並且連通所有的結點,因此 T 必須是一棵樹。我們稱這樣的樹為(圖G的)生成樹。因為它是由圖 G 所生成的。我們稱求該生成樹的問題為“最小生成樹問題”,如下圖

這裡寫圖片描述

關於下面要討論的最小生成樹演算法

下面討論最小生成樹問題的兩種演算法:Kruskal 和 Prim。如果使用普通的二叉樹,那麼可以很容易地將兩個演算法的時間複雜度限制在 O(ElgV) 的數量級內。但如果使用斐波那契堆,Prim演算法的執行時間將改善為 O(E + VlgV)。此執行時間在 |V| 遠遠 小於 |E| 的情況下較二叉堆有很大改進(也就是“結點數”遠遠小於“邊數”)。

  • 普通情況下的 Kruskal 和 Prim 時間複雜度:O(ElgV)
  • |V| 遠遠 小於 |E| 並且 使用 Prim 演算法的時間複雜度:O(E + VlgV)

第二種的時間複雜度上,因為|V| 遠遠 小於 |E|,所以 VlgV 也遠遠小於 ElgV。

這兩種演算法都是貪心演算法。

23.1 最小生成樹的形成

我們先把前提條件說一下:

假設有一個連通無向圖 G=(V, E) 和權重函式 w: E->R (這個函式個人理解是 w(E)=R 這個意思,R 是函式的值)

我們希望找出圖 G 的一棵最小生成樹。

本章的兩個演算法都使用貪心策略來解決問題,方式不同,但可以使用下面的通過方法來表示大概。

這裡寫圖片描述

A 是某棵最小生成樹的一個子集。每一步我們要做的事情是選擇一條邊 (u, v),將其加到集合 A 中,使得 A 不違反迴圈不變式,即 A 並 {(u, v)} 也是某棵最小生成樹的子集。由於我們可以安全地將這種邊加入到集合 A 而不會破壞 A 的迴圈不變式,因此稱這樣的邊為集合 A 的安全邊。

上面程式碼中,第3行是關鍵。第3行是要找到一條安全邊。迴圈不變式告訴我們存在一棵生成樹 T ,滿足 A ⊆ T。在 while 體內,集合 A 一定是 T 的真子集,因此必然存在一條邊 (u, v) ∈ T,使得 (u, v) ∉ A,並且 (u, v) 對於集合 A 是安全的。

關於一些定義

  • 切割:一個切割就是一個條用於劃分兩部分的線。例如:切割(S, V-S) 是把結點分成兩部分,一部分結點集合是 S,另一部分就是 V-S(也就是結點總集合 V 減去 剛才的結點集合 S)。可以看圖 (a)
  • 橫跨切割:如果一條邊 (u, v) 屬於 E 的一個端點位於集合 S,另一個端點位於集合 V-S,則稱該條邊橫跨切割(S, V-S)。
  • Respect:如果集合 A 中不存在橫跨該切割的邊,則稱該切割 respect 集合A。這裡 respect 感覺應該翻譯成“分隔”的意思。因為集合A 沒有任何邊和“切割”相交,那麼它就被分隔在“切割”的一面了。
  • 輕量級邊:橫跨切割(S, V-S) 的所有邊中,權重最小的邊稱為“輕量級邊”。注意,輕量級邊並不是唯一的。如果一條邊是滿足某個性質的所有邊中權重最小的,則稱該條邊是滿足給定性質的一條輕量級邊。

感覺“切割”如果翻譯成“切割線”更好一些。

這裡寫圖片描述

23.2 Kruskal 和 Prim 演算法

本節對兩個經典演算法進行討論。每種演算法都是一條具體的規則來確定 GENERIC-MST 演算法第 3 行所描述的安全邊。

  • 在 Kruskal 演算法中,集合A 是一個森林,其結點就是給定圖的結點。每次加入到集合A 中的安全邊永遠是權重最小的連線兩個不同部分的邊。
  • 在 Prim 演算法中,集合A 則一棵樹。每次加入到 A 中的安全邊就遠是連線 A 和 A 之外的某個結點的的邊中權重最小的邊。

Kruskal 演算法

主要思想就是,取最小權重的邊,然後進行合併。具體內容如下:

  • 先把每條邊按權重進行升序排序
  • 然後對排完序每條邊進行迭代
    • 如果當前這條邊的兩個結點不是在同一棵樹裡,就把“兩個結點所表示的樹”進行合併,再加到返回集合中

因為每次取的邊都是“剩下邊中”權重最小的邊,所以屬於貪心演算法。

Kruskal 演算法的實現與 21.1 節所討論的計算連通分量的演算法類似。我們使用一個不相交集合資料結構來維護幾個互不相交的元素集合。每個集合代表當前森林中的一棵樹。

  • 使用FIND-SET(u) 用來返回包含元素 u 的集合的代表元素。我們可以通過測試 FIND-SET(u) 是否等於 FIND-SET(v) 來判斷結點 u 和 v 是否屬於同一棵樹。
  • 使用 UNION 來對兩棵樹進行合併。

這裡寫圖片描述

這裡寫圖片描述

Prim 演算法

主要思想:

  • 從所有結點中,任意選擇一個結點 r 。
  • 結點 r 連線的所有結點的邊中,選擇一個權重最小的邊,加入到 r 所在的集合。
  • 一直到所有結點都被加進來為止。

這裡寫圖片描述

程式如下:

這裡寫圖片描述

關於程式還有一些要解釋的:

  • 這個程式是使用“父指標”的方式,把所有結點連成了一棵樹。並不是像 Kruskal 程式一樣,用一個集合來儲存所有的邊。
  • v.key 儲存的是“和 v 連線的邊”的權重。例如:結點b 和 a、c、h 連線,所有它有 3 個權重可以選擇 a-4、c-7、h-11。b.key 裡應該儲存 4,但因為最開始的結點的不確定性,所以可能最開始儲存的不是 4,而是其它的權重,但最後終於儲存最小權重。
  • 迭代方式,是使用最小堆的方式。
    • 首先把所有的元素都放到堆裡,以元素的 key 屬性作為堆的比較值。在放到堆裡時,把任意指定的結點的 key 屬性設定成 0,其它結點設定成無窮大,這樣在從最小堆裡取得最小元素時,就會把這個“任意指定”的結點第一個取出來處理。(程式1~4行是設定 key 的地方)
    • 然後在再從堆裡取得最小結點 u,然後把結點 u 的連結串列中的結點 v 每個都取出來,進行設定:如果 v 還在 堆中 並且 w(u, v) < v.key(u和v 的權重 < v 現在的權重),就設定 v 的父結點 和 key 的值。(設定完後,v 可能還在堆裡,這樣下次取得堆裡最小 key的結點時候,就可能把它取出來)
    • 迴圈上一步,直到堆裡所有的元素都處理完畢,變成堆裡沒有任何元素。
  • 關於在判斷 v 是否在還在堆中的問題,由於堆一般沒有“查詢”方法,所以這個判斷是由一個標誌位來做的。在從堆中刪除結點的時候,設定該標誌位。
  • 在第7行取得堆中最小元素時,就是取得某條橫跨“切割(V-Q, Q)”的輕量級邊的一個端點。因為在最開始時,所有的結點都在堆Q 中,所以 (V-Q, Q) = (0, Q)。從堆Q 中取得一個元素後,切割(V-Q, Q) 就在新取出的結點u 和其它結點中間了,這樣 u 連線哪個結點,都是橫跨切割的。(至少看到這,沒看到切割和橫跨切割的概念對我們有什麼作用。。。)

Prim 演算法的執行時間取決於最小堆Q的實現方式。

如果將 Q 實現為一個二叉最小堆:

  • 可以使用 BUILD-MIN-HEAP 來執行演算法的第1~5行,時間成本為 O(V)。while 迴圈中的語句一共要執行 |V| 次,由於每個 EXTRACT-MIN 操作需要的時間成本為 O(lgV),所以 EXTRACT-MIN 操作需要的總共時間成本為 O(VlgV)。
  • 所有鄰接連結串列長度之和為 2|E|,演算法8~11行的 for 迴圈的總執行次數為 O(E)。在 for 迴圈裡面,可以使用常數時間判斷”一個結點是否屬於Q“
  • 第11行的賦值操作,還隱含 DECREASE-KEY 操作(因為需要把修改後的的 key 和 π 儲存到結點上),該操作在二叉最小堆上的執行時間為 O(lgV)。因此 Prim 演算法總時間為:O(VlgV + ElgV) = O(ElgV)。從漸近意義上來說,它與 Kruskal 的執行時間相同。

如果使用斐波那契堆來實現最小優先堆Q,Prim 演算法的漸近執行時間可以得到進一步改善:

  • EXTRACT-MIN 操作時間的攤還代價為 O(lgV)(最小堆的時間為O(VlgV),少個倍數V),而 DESCRASE-KEY 操作的攤還時間為 O(1)(最小堆的時間為O(lgV))。
  • 使用斐波那契堆實現最小堆的話,時間複雜度改善為 O(E + VlgV)

參考:

todo:

  • 看一下那個最容易明白的部落格上的最小生成樹文章。
  • 為什麼所有鄰接連結串列長度之和為 2|E|
  • 為什麼斐波那契堆的 DESCRASE-KEY 的攤還時間為 O(1)

第二十四章 單源最短路徑

Patrick 教授希望找到一條從菲尼克斯到印第安納波利斯的最短路徑,如何才能找出這樣一條最短路徑呢?

一種可能的辦法是,先將從菲尼克斯到印第安納波利斯的所有路徑都找出來,將每條路徑上的距離都累加起來,然後選擇一條最短路徑。但是即使在不允許環路的情況下,也需要檢查無數種可能的路徑,其中大部分都不值得檢查。例如,一條從菲尼克斯經過西雅圖再到印第安納波利斯的路徑顯然不符合要求,因為西雅圖已經偏離了目標方向好幾英里。

本章及第25章將闡述,如何高效地解決這個問題。在最短路徑問題中,我們給定一個帶權重的有向圖 G=(V, E) 和權重函式 w:E->R,該權重函式將每條邊對映到實數值的權重上。圖中一條路徑 p=(v0, v1, …, vk) 的權重 w(p) 是構成該路徑的所有邊的權重之和:

w(p)=i=1kw(vi1,vi)

這個公式就是 w(v0,v1) + w(v1,v2) + … + w(vk-1,vk) 的和。

定義從結點 u 到 v 的最短路徑權重 δ(u,v) 如下:

這裡寫圖片描述

在求菲尼克斯到印第安納波利斯的最短路徑的例子中,我們可以用一幅圖來表示道路的交通圖:結點代表城市,邊代表城市之間的道路,邊上的權重代表道路的長度。我們的目標就是找出一條給定城市菲尼克斯到印第安納波利斯的最短路徑。(當然權重也可以代表非距離度量單位,如:時間,成本,罰款,損失或其它)

為瞭解決較為廣義的情境,接下來討論的最短路徑問題將考慮的是一個帶權重的有向圖,以權重總和表示路徑成本,並以具有方向性的邊表示兩個結點之間的關係。

  • 無向圖的問題能夠以有向圖的模型解決,反之則無法。
  • 不具權重的邊也能夠以帶權重的邊模擬(將全部權重設定成相同值即可),反之則無法。
  • 可以視為只能處理無權重圖的“廣度優先”和“深度優先”的擴充包。(不有理解什麼意思)

最短路徑的幾個變體

  • 單源最短路徑問題:這是我們集中精力討論的問題。簡單地說,就是找出從一個源結點 s 到圖中各個結點 v 的最短路徑。單源最短路徑問題可以用來解決許多其他問題,其中就包括下面的幾個最短路徑問題。
  • 單目的地最短路徑問題:找到從每個結點 v 到“給定目的地”結點 t 的最短路徑。如果將圖的每條邊的方向翻轉過來,我們就可以將這個問題轉換為單源最短路徑問題。是單源最短路徑問題的變種。
  • 單結點對最短路徑問題:找到從給定結點 u 到 v 的最短路徑。如果解決了針對單個結點 u 的單源最短路徑問題,那麼也就解了這個問題。而且,在該問題的所有已知演算法中,最壞情況下的漸近執行時間都和最好的單源最短路徑演算法的執行時間一樣。是單源最短路徑問題子問題。
  • 所有結點對最短路徑問題:對於每對結點 u 和 v,找到從