該博客原地址http://blog.csdn.net/ntt5667781/article/details/52743342
其實就是對書上的內容做了個總結
在開始各類圖算法之前,先將圖的結構進行分類。
圖的表示,在實際實現過程中,有以下幾種基本的方式可以來表示圖。
1) 鄰接矩陣:對於較小或者中等規模的圖的構造較爲適用,因爲需要V*V大小的空間。
2) 邊的數組:使用一個簡單的自定義edge類,還有兩個變量,分別代表邊的兩個端點編號,實現簡單,但是在求每個點的鄰接點的時候實現較爲困難。
3) 鄰接表數組:較爲常用,使用一個以頂點爲索引的數組,數組每個元素都是和該頂點相鄰的頂點列表,這種數組佔空間相對於鄰接矩陣少了很多,並且能很好的找到某個給定點的所有鄰接點。
按照圖中邊的方向將圖分成有向圖和無向圖:
1)無向圖:圖中的邊沒有方向。
2)有向圖:圖中的邊有方向。
對於有向圖和無向圖的具體實現表示可以使用前面介紹的三種方法,兩種圖在表示的時候大部分的實現代碼都是一致的。
普通無向圖的鄰接數組表示方法的具體實現代碼:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
無權有向圖的具體實現代碼:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
一 圖的遍歷算法:
介紹兩種比較基礎的圖遍歷算法,廣度優先搜索和深度優先搜索。
1)深度優先搜索:這是一種典型的遞歸算法用來搜索圖(遍歷所有的頂點);
思想:從圖的某個頂點i開始,將頂點i標記爲已訪問頂點,並將訪問頂點i的鄰接列表中沒有被標記的頂點j,將頂點j標記爲已訪問,並在訪問頂點j的鄰接列表中未被標記的頂點k依次深度遍歷下去,直到某個點的所有鄰接列表中的點都被標記爲已訪問後,返回上層。重複以上過程直到圖中的所有頂點都被標記爲已訪問。
深度優先遍歷和樹的先序訪問非常類似,儘可能深的去訪問節點。深度優先遍歷的大致過程(遞歸版本):
a)在訪問一個節點的時候,將其設置爲已訪問。
b)遞歸的訪問被標記頂點的鄰接列表中沒有被標記的所有頂點
(非遞歸版本):
圖的非遞歸遍歷我們藉助棧來實現。
a)如果棧爲空,則退出程序,否則,訪問棧頂節點,但不彈出棧點節點。
b)如果棧頂節點的所有直接鄰接點都已訪問過,則彈出棧頂節點,否則,將該棧頂節點的未訪問的其中一個鄰接點壓入棧,同時,標記該鄰接點爲已訪問,繼續步驟a。
該算法訪問頂點的順序是和圖的表示有關的,而不只是和圖的結構或者是算法有關。
深度優先探索是個簡單的遞歸算法(當然藉助棧也可以實現非遞歸的版本),但是卻能有效的處理很多和圖有關的任務,比如:
a) 連通性:ex:給定的兩個頂點是否聯通 or 這個圖有幾個聯通子圖。
b) 單點路徑:給定一幅圖和一個固定的起點,尋找從s到達給定點的路徑是否存在,若存在,找出這條路徑。
尋找路徑:
爲了實現這個功能,需要在上面實現的深度優先搜索中中增加實例變量edgeTo[],它相當於繩索的功能,這個數組可以找到從每個與起始點聯通的頂點回到起始點的路徑(具體實現的思路非常巧妙: 從邊v-w第一次訪問w的時候,將edgeTo[w]的值跟新爲v來記住這條道路,換句話說,v-w是從s到w的路徑上最後一條已知的邊,這樣搜索結果就是一條以起始點爲根結點的樹,也就是edgeTo[]是個有父鏈接表示的樹。)
深度優先搜索的遞歸實現版本和非遞歸版本(遞歸是接住了遞歸中的隱藏棧來實現的。非遞歸,藉助棧實現)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
已經使用DFS解決了一些問題,DFS其實還可以解決很多在無向圖中的基礎性問題,譬如:
1)計算圖中的連通分支的數量;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
2)環檢測:檢測圖中是否有環;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
3)二分圖判斷(雙色問題):能否用兩種顏色給這個二分圖進行着色,也就是說這個圖是不是二分圖。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
2)廣度優先搜索:
前面說過,深度優先搜索得到的路徑不僅取決於圖的結構,還取決於圖的表示以及遞歸調用的性質,但是如果要求最短的路徑(給定圖G和起始點s尋找給定點v和s間是否存在路徑,如果存在,找出最短的路徑),那麼使用前面的DFS算法並不能解決該問題,所以出現了廣度優先搜索BFS來實現這個目的,廣度優先搜索也是其他算法的基礎。
在程序中,搜索一幅圖的時候會遇到有很多條邊都需要被遍歷的情況,我們會選擇其中一條並將其他邊留到以後再繼續搜索,在DFS中使用棧結構,使用LIFO的規則來描述,從有待搜索的通道中選取最晚遇到的那個通道,然而在BFS算法中,我們希望按照與起點的距離來遍歷所有的頂點,使用FIFO(隊列)來進行搜索,也就是搜索最先遇到的那個通道。
BFS:使用一個隊列來保存所有已經被標記過的但是其鄰接點還未被檢查過的頂點,現將頂點加入隊列中,然後重複下面的操作,直至隊列爲空:
1)取隊列中的下一個頂點v並標記它
2)將與v相鄰的所有的未被標記的頂點加入隊列中。
廣度優先搜索類似於樹的按層遍歷
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
DFS和BFS是兩種基礎的通用的圖搜索算法,在搜索中我們都運用以下方法:
將起始點添加入某個數據結構中,然後重複以下步驟直至數據結構中的所有數據都被清空。
1) 取數據結構的下個數據v並且標記它。
2) 將v所有的相鄰的未被標記的頂點加入到數據結構中。
這兩種算法每次都擴展一個節點的所有子節點,而不同的是,深度搜索下一次擴展的是本次擴展出來的子節點中的一個,而廣度搜索擴展的則是本次擴展的節點的兄弟節點
使用範圍:DFS可以迅速的找到一個解,然後利用這個解進行剪枝,而BFS可找到最優解。
將上述的圖像數據類型改成有向圖就可以實現有向圖中的遍歷問題。
在有向圖中單點的聯通問題(即給定的兩點是否聯通)變成了可達問題(即對於給定的兩個是否存在一條有向路)在有向圖中,使用完全相同的代碼,在就可以在有向圖中就可以解決可達問題。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
然而在有向圖中的環檢測就和無向圖中不大一樣,需要保存某條道路上的所有信息,來判斷是否形成有向環。
在執行dfs(G,s)的時候查找的總是一條由起點到達s的有向路徑,要保存這條路徑,程序實現的時候維護了一個由頂點索引的boolean類型數組onStack用來標記遞歸調用棧上的所有頂點(在dfs調用的時候,將onStack[s]設置成true,結束的時候(就是這條有向路結束了)再將其設置爲false)當它在找到一條邊v→w時候,w已經在棧中(這裏的是表示這個點已經在這條路上出現過了,也就是在onStack中已經標記過了)它就找到可一個有向環,環上的頂點和無向圖一樣可以通過edgeTo數組獲得。這裏在檢測遇到的點w是否在棧中,不像無向圖那樣檢查是否在marked中標記過,是因爲,在有向圖中的環必須是一條首尾結點一樣的有向路,環上的所有有向邊的方向必須一致。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
3)拓撲排序:
拓撲排序:給定一幅有向圖,給所有的結點排序,排序後使得有向邊均從排在前面的結點元素指向排在後面的結點元素(或者說明這個有向圖不能進行拓撲排序)。在對一個有向圖進行拓撲排序的時候,必須保證它是無環的有向圖,因爲有環的圖不能做到拓撲有序。
其實對於標準的深度優先搜索算法添加一行代碼就能實現這個問題。在使用深度優先搜索的時候,正好只會訪問每個結點一次,如果將dfs()訪問的結點存儲在一個數據結構中,然後遍歷這個結構就可以訪問圖中所有結點,遍歷的順序取決於這個數據結構的性質以及是在遞歸前還是遞歸後保存
1) 前序:在遞歸前將頂點放入隊列中
2) 後序:在遞歸調用之後將頂點放入隊列中
3) 逆後序:在遞歸調用之後將頂點壓入棧中。
一幅有序無環圖的拓撲排序就是所有頂點的逆後序排列。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
拓撲排序實現代碼:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
使用深度優先搜索進行拓撲排序,其實是使用了兩遍深度優先搜索,一遍是查看有向圖中是否存在有向環第二遍就是產生頂點的逆後序。由於兩次都訪問了所有的頂點和邊,所以這個算法需要的時間是和V+E成正比的。
深度優先搜索需要先預處理,而它使用的時間和空間與V+E成正比。且在常數時間內處理關於圖的連通性。Union-find也可以進行圖搜索,但是實際上,union-find其實更加快,因爲不需要構造並表示一幅圖,而且union-find算法是一種動態算法,但是深度優先算法需要對圖進行預處理。我們在完成只需要判斷連通性或是需要完成大量的連通性查詢和插入操作混合等類似的任務時,更加傾向於使用union-find算法,而深度優先算法則跟適合實現圖的抽象數據類型。
補充說明:Union-find算法(解決動態連通性問題):
問題描述:給出一列整數對,每個整數對(p,q)可以被認爲是相連的,我們假定相連是一種等價關係(這種相連的屬性滿足自反性,對稱性以及傳遞性,從圖的角度來看,這個整數對可以看作無向邊的兩個端點)。這種等價關係將輸入的數據對劃分爲多個等價類(從圖的角度來看,是將他們劃分爲多個連通分量)我們需要設計一種數據結構,來保存已經輸入的數據對象(即已知的數據對)的所有信息,並用這些信息來判斷,新的數據對是否是相連的(也就是新添加進的邊的兩個端點是否在同一個連通分支內),將這個問題通俗的稱爲動態連通性問題,這和我們前面所講的使用DFS來判斷圖的連通性可以達到一樣的目的,但是這個是在動態生成的過程中判斷圖的連通性,而DFS需要預處理整個圖,不能在動態的過程中來判斷。
union-find 算法中API:
1)void union(int v,int w) 在p和q之間添加一條連接
2)int find(int v) p所在的分量的標識
3)boolean connected(int v,int w)如果p和q在同一個分量中,那麼返回true
4)int count() 返回連通分量的數目;
在具體實現過程中,選擇一個以頂點爲索引的數組,來記錄對應頂點所在的聯通分支的標識(將使用連通分量中某個頂點作爲該分支的標識),在union中,如果p和q不在一個連通分量中,那麼需要更具不同的算法改變其id數組中的值,如果在同一分量中,忽略不計即可。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
這裏將介紹三種find-union的實現方法(每種方法只是find和union實現不太相同),但是他們都是根據以頂點爲索引的id數組來確定在不在同一連通分量中:
1)quick-find方法:
在這種方法中,同一個連通分量中的id值都是相同的。 在find實現的時候,直接返回id中的值即可。在union實現的時候,先判斷給定的數據對是否在同一個連通分量中。如果是就直接忽略,否則,合併p和q所在的連通分量(就是將p所在的連通分量的標識id全部替換成q所在的連通分量標識id(反之亦可)),爲此,我們需要遍歷整個數組;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
2)quick-union方法:
由上述可知,我們對於每對輸入都需要遍歷整個數組,因此quick-find無法處理大型數據,假設使用quick-find方法並且最後只得到一個連通分支,那麼至少需要調用N-1次union方法,每次union方法都需要至少N+3次操作,那麼整個算法的性能就是平法級別的。而quick-union方法提高了union的效率,同樣也是基於相同的數據結構-由頂點索引的id數組,但是該方法中的id數組的意義有所不同,這裏的id[]中的元素都是同一個分量中其他頂點的編號,也有可能是自己,我們將這種聯繫稱爲鏈接。在find方法中,我們從給定的頂點開始,由它的鏈接得到另外一個頂點,再由這個頂點的鏈接得到新的頂點,如此繼續跟隨頂點的連接到達新的頂點,直到鏈接指向自己(這種鏈接所對應的頂點被稱爲跟結點),和quick-find不同,quick-union只有兩個頂點開始這一過程並且到達相同的跟結點,才能說它們是同一個分支中的。所以在具體的union實現中,我們需要按照上述過程找尋給定的兩個頂點的跟結點,如果跟結點相同(說明這兩個頂點在同一個分量中),否則,將其中一個根結點中的鏈接連接到另外一個跟結點上(也就是爲其中一個跟結點的鏈接重新定義爲另外一個跟結點)這種實現find-union方式被稱爲quick-union方法。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
3)加權的quick-union:
但是在前面講的quick-union方法中,可能會出現根據不同的輸入可能出現最壞的情況,就是樹偏向一邊,也就是實現仍然需要平方級別的的消耗,在上述實現思想中,再做個輕微的改進就能極大的提高算法的效率,就是每次合併兩個分量的時候,不是隨意的合併,而是將較小的分量的跟結點的鏈接改爲較大跟結點。這樣就可以在以某種程度上達到平衡性,這裏需要維護一個size數組,使得跟結點對應索引的元素的值爲這個分量的大小(即分量中元素的個數)每次在進行合併需要改變id值得時候,比較兩個分量的跟結點的所在的分量的大小,總是將小的分量的鏈接改爲大的分量的跟結點,並且跟新新的合成分量的跟結點的大小。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
4)使用路徑壓縮的加權quick-union方法:
在這種方法中,使得每個節點都直接連接在其跟結點上,但是我們又不想像quick-find那樣遍歷整個數組,因此,在檢查頂點的同時將他們直接連接在跟結點上,要想實現路徑壓縮,只需要爲find方法加上一個循環,將在路上遇到的所有節點都連接到跟結點上。
二.最小生成樹:
在下面的討論中做出如下約定:
1) 只考慮連通圖;
2) 邊的權重可以是任何數;
3) 所有邊的權重都各不相同(如果存在權值相同的邊,最小生成樹不唯一);
切分定理(解決最小生成樹的所有算法的基礎):
將加權圖中的所有頂點分成兩個集合(兩個非空且不重合的集合),檢查橫跨兩個集合的所有邊(這種邊被稱爲橫切邊:一條連接兩個不屬於同一集合頂點的邊),並識別那條邊是否應該屬於圖的最小生成樹。
切分定理:在一幅加權圖中,給定任意的劃分,它的橫切邊中權值最小者必然屬於最小生成樹。(證明:使用反證法,假設e爲權值最小的橫切邊,T爲圖的最小生成樹。如果T中不包含e,那麼必然包含一條橫切邊f,將e邊加入最小生成樹中,形成了一個環,包含e, f邊,那麼將f從環中刪去,生成一個新的生成樹T’顯然,新的生成樹比原來的最小生成樹更小,這與已知矛盾)
從切分定理可知,(在假設前提下,所有邊的權值都不同,那麼最小生成樹是唯一的)對於每一種切分,權值最小的橫切邊必然屬於最小生成樹。但是,權值最小的橫切邊並不是所有橫切邊中唯一屬於圖的最小生成樹的邊,實際中,一次切分產生的橫切邊可能有多條屬於最小生成樹。
所有求解最小生成樹的算法都是使用的貪心策略(根據切分定理,每次選擇一種劃分,使得所有的橫切邊都沒有被標記,那麼選擇權值最小的橫切邊,直至選擇了V-1條邊爲止。只不過對於不同的算法所使用的切分方法和判斷權值最小的橫切邊的方式有所不同。)
1)Prim算法:
思想:最開始樹中只有一個頂點,每次爲生長中的樹添加一個邊,直至添加了V-1條邊爲止,每次添加的邊都是樹中的頂點和非樹中的頂點所劃分的兩個集合的橫切邊中最小的邊。
在具體實現中使用一些簡單的數據結構來實現樹中點的標記(使用boolean類型的頂點索引數組來標記頂點是否在樹中),待選擇橫切邊(使用優先隊列來處理帶選擇的橫切邊,根據橫切邊的權重),生成樹中的邊(頂點索引的Edge對象數組來保存最小生成樹中的邊)。
我們使用一個方法來爲樹添加頂點,將這個頂點標記爲已訪問,並且將與它關聯的所有未失效的(連接新加入的節點和其他已經在樹中的頂點的所有邊失效)邊加入優先隊列中。然後取出優先隊列中權值最小的邊,並且檢查這個邊是否有效,如果有效,將這個邊的不在樹中的點標記爲已訪問,並將這個邊加入最小生成樹的邊集合中,然後從優先隊列中刪除這個邊。調用新的頂點來更新橫切邊集合。
prim算法的延時實現代碼:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
優化思想:改進Prim的延時實現,可以嘗試在優先隊列中刪除失效的邊,這樣優先隊列就可以只包含真正的橫切邊。關鍵的優化思想在於:需要在意的只是連接樹頂點和非樹頂點中權重最小的邊,也就是說,當我們將V添加到生成樹頂點集合中去後,對於每個非樹頂點w不用保存w到樹的每條邊,只需要保存權重最小的那個邊。也就是說,在優先隊列中,只需要保存每個非樹頂點的一條邊:將它和樹中頂點連接起來權重最小的那個邊,其他權重較大的邊遲早會失效。
即使實現的Prim算法:使用兩個頂點索引數組edgeTo[] 和distTo[]來替換延時實現中的marked和mst數據結構;
EdgeTo如果頂點v不在樹中,但是至少含有一條邊和樹相連,那麼edgeTo[v] 就是將v和樹連接的最小權重的邊,而這個distTo則是這條邊的權重。
將這類頂點v都保存在一條索引優先的隊列中,索引v關聯的值是edgeTo的邊的權值。
2)Kruskal算法:
思想:按照邊的權重順序來處理它們,依次將當前權值最小的邊加入生成樹的邊中(加入的邊不能和已經加入的邊構成環)直至加入了V-1條邊。在整個過程中,加入的邊組成的森林隨着新加入的邊漸漸合成一棵樹。
使用一個優先隊列來將所有的邊(也就是爲所有邊按照權值排序),然後再使用union-find數據結構識別新加入的邊是否會和已有的樹中的邊形成環(由於這是在動態處理中識別環是否存在,這裏使用union-find(由於深度優先算法需要預處理整個圖,在這裏不是很適用),最後用一個隊列或者別的數據結構來保存最小生成樹的所有邊。
三.最短路徑算法:
在具體實現中使用到的類型結構;
1) 帶有權重的有向邊:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
2) 加權有向圖:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
在具體實踐中用到的數據結構:
1) 最短路徑樹的邊:edgeTo[v]:由頂點索引的DirectedEdge的數組,其中edgeTo[v]的值是樹中連接v和它的父節點的邊
2) 到達起點的距離:distTo[]數組其中distTo[v]代表從點v到達起點的已知的最短路徑;
邊的鬆弛:
最短路徑的API都基於一個鬆弛操作,放鬆邊v→w意味着檢查從s(起點)到w的最短輪徑是否先到達v,再從v→w,如果是,則根據這個情況來跟新數據結構的內容。也就是,那麼distTo[v]與邊v→w邊的權重的和就可能成爲新的s到達w的最短路徑,並且跟新distTo[v],也就是說distTo[v]+e(vw).weight < distTo[w],否則就說這條邊失效了,忽略它。
頂點的鬆弛:
將一個頂點所連接的所有邊進行鬆弛操作(這裏的邊鬆弛和上述過程一樣)。
1)Dijkstra算法:
該算法採用了的思想和在求最小生成樹時候使用的prim算法類似的思想,首先將distTo[s]設置爲0,distTo的其他元素設置爲正無窮大,然後將distTo中最小的非樹頂點放鬆並加入到樹中,如此這般,直至所有頂點都在樹中,或者所有非樹頂點的distTo都無窮大。
Dijkstra能解決邊權值非負的加權的有向圖的最短路徑問題:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
2)無環加權有向圖中的最短路徑算法:
對比Dijstra算法來說,對於無環加權有向圖的最短路徑有個好的改進算法,該算法:
a) 能夠在線性時間內解決單點最短路徑;
b) 能夠處理權值爲負的邊
c) 能夠解決相關問題(譬如,最長路徑求解)
這些都是在無環有向圖的拓撲排序算法的簡單擴展,特別的是,將頂點放鬆和拓撲排序結合起來就能得到這種解決無環加權有向圖的最短路徑的簡單算法。
首先將distTo[s]初始化爲0,其他distTo數組元素初始化正無窮大,然後按照拓撲排序來一個個放鬆頂點。(這種方法能在於V+E成正比的時間內解決無環加權有向圖的最短路徑問題)
對於無環問題來說,這種拓撲排序和放鬆相結合的算法,大大簡化了問題的判斷,而且這種算法和邊的權值的正負無關。但是隻能適用於無環結構(有環結構不能進行拓撲排序)。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
使用上面的算法還能快速的解決無環加權有向圖的單點最長路徑問題。解決無環加權有向圖的最長路徑問題需要的時間和E+V成正比。(證明,複製原來的無環加權有向圖的一個副本,並把副本中的所有邊的權值取相反數,這樣副本中的最短路徑就是原圖中的最長路徑。其實在、實現的一個更簡單的方法就是,將distTo的初始值變爲負無窮,然後改變鬆弛的條件方向)
對於這個算法還可以運用在優先級限制下的並行任務調度(這就是個無環加權有向圖):
問題描述:給定一組需要完成的任務及其需要完成的時間,一級乙組關於任務完成的優先級限制順序。在滿足優先級限制的條件下,應該如何在若干個處理器上安排任務並在最短的時間內完成所有的任務。存在一個線性時間內的算法“關鍵路徑的方法被證明和無環加權有向圖的最長路徑問題是等價的。
解決並行任務調度的關鍵路徑方法的步驟如下:創建一副無環加權的有向圖,其中包含一個起點s和一個終點t且每個任務都對應着兩個頂點(一個起始頂點,一個任務結束頂點,兩個頂點間邊的長度爲任務完成所需要的時間)對於每條優先級限制v→w添加一條從v到w的權重爲0的邊,還需要爲每個任務添加一個從起始點s指向該任務的起始點的權重爲0的邊,以及一條從該任務的結束點到達終點t的權重爲0的邊。這樣,每個任務的預計開始時間就是從起始點s到達該任務起始點的最長距離。
3)一般加權有向圖中的最短路徑算法(BellmanFord算法):
思想:在任意含有V個頂點的加權有向圖中給定起始點s,從s無法到達任何負權重環,以下算法就能解決其中的單點最短路徑問題,將distTo[s]初始化爲0,其他元素的distTo初始化爲正無窮大,然後以任意順序放鬆有向圖中的所有邊,重複V輪。但是根據經驗,我們會很容易的得出,任意一輪中許多邊的放鬆都不會成功,只有上一輪中distTo值發生變化的頂點連接的邊才能夠改變其他distTo中的元素值,我們可以使用FIFO隊列來紀錄發生變化的頂點。
實現的時候使用下面兩種數據結構:
1)一條用來保存即將被放鬆的頂點的隊列;
2)一個由頂點索引的boolean數組,用來指示頂點是否在隊列中,防止重複的往隊列中添加頂點。
爲了完整的實現V輪候算法能夠終止,實現這個目的一種方法就是顯示的紀錄輪數。