1. 圖表算法
本篇隨筆寫的是圖表算法。圖表可以大致分為兩種:無向圖和有向圖。
無向圖例子:
有向圖例子:
從上述例子中可以看出,一個圖表是由數個頂點和邊組成的。
其中,無向圖的邊是沒方向的,即兩個相連的頂點可以互相抵達。
而有向圖的邊是有方向的,即兩個相連的頂點,根據邊的方向,只能由一個頂點通向另一個頂點。(當然,如有向圖例子中的2和3,由於有兩個指向對方的方向,所以2和3是互通的。)
本文寫的是無向圖的算法,有向圖的算法將寫在下篇隨筆中。
2. 無向圖(UndirectedGraph)
要討論無向圖的算法前,首先要討論如何創建一個無向圖。
創建無向圖有兩個關鍵點:
a. 這個無向圖有哪些點
b. 哪些點可以通往哪些點
舉個例子:
顯然,這個無向圖有13個點。0可以通往1,2,5,6;6可以通往0,4;5可以通往0,3,4。
即0的鄰居點有1,2,5,6;6的鄰居點有0,4。
那么,我們怎么告訴程序這個無向圖呢?
目前有兩種主流的方法:鄰接矩陣(Adjacency-matrix)和鄰接列表(Adjacency-list)。
鄰接矩陣:
如果兩個點相鄰,則用1表示,否則用0。例如1是0的鄰居點,所以0和1相交對應的格子為1。
從這個圖可以看出,如果每個點不是與大量的其它點互為鄰居,則會有很多0出現。如果點的數量龐大,矩陣將十分巨大,且有很多空間浪費(0占據的格子)。
鄰接列表:
就是用一個數組把所有點裝起來,每個位置是由對應點的所有鄰居點形成的數組。
即adj[0]=6,2,1,5(0可以通往1,2,5,6);adj[6]=0,4(6可以通往0,4)。
這個列表比較適合每個點只與少量其它點相鄰的情況。
我們可以根據實際情況進行選擇用列表還是矩陣。
通過創建一個矩陣或者列表,程序可以知道這個無向圖有哪些點和點與點之間的聯系。如果要加點或者刪點,應該不難實現,這里不做詳述。
接下來,我們討論算法:深度優先搜索(depth-first search)和廣度優先搜索(breadth-first search)。
3.深度優先搜索(depth-first search)
深度優先搜索可以解決的問題有:
a. 給定一個點,求所有它能抵達的所有點。
b. 給定兩個點,它們是否能抵達彼此,如果能,求路線。
等等。
這個算法簡單地用一句話概括就是:一路走到盡頭,然后返回上一個分支,走另一條路到盡頭,再返回,再走其他路,直到全部走完為止。
從例子入手:
為了方便講解,作下圖:
0~12代表着圖中的所有點。
一開始所有點標記為False(F),當我們走到某個點后,此點標記為True(T)。標記過的點不需要再走一次。
EdgeTo記錄了部分路線,所有部分路線可以整合成一個完整的路線。例如從E點抵達A點,則記為EdgeTo[A]=E;
Group記錄了這些點形成了多少個組,且哪些點分別屬於哪些組。這樣可以瞬間查到兩個點是否相連。例如本例中分為3組,其中7,8屬於一組;9,10,11,12屬於第二組;剩下的屬於第三組。
先看最大的這組:
從0開始,先把0標記了(變紅)。記0為第0組。
0的鄰居點有4個,隨便選一條路來走。例如去2:2標記為True;2從0來,故EdgeTo[2]=0; 2與0屬於同一組:0組。
然后2只有一個鄰居點0,但0已標記為T,故不用走。2已經無路可走了,返回上一個分支點0。
0再選擇走其它路:6。6標記為True;6從0來,故EdgeTo[6]=0; 6與0屬於同一組:0組。
然后6有兩個鄰居點:0和4。由於0已標記為T,不走。
去4:4標記為True;4從6來,故EdgeTo[4]=6; 4與6屬於同一組:0組。
然后4有三個鄰居點:6,3,5。由於6已標記為T,不走。
隨便選一個路走:5。5標記為True;5從4來,故EdgeTo[5]=4; 5與4屬於同一組:0組。
然后5有三個鄰居點:0,3,4。由於0,4已標記為T,不走。
只能走3:3標記為True;3從5來,故EdgeTo[3]=5; 3與5屬於同一組:0組。
然后3有兩個鄰居點:5和4。由於4,5已標記為T,不走。
3無路可走,返回上一個分支5。5有三個鄰居點:0,3,4。由於0,3,4已標記為T,不走。
5無路可走,返回上一個分支4。4有三個鄰居點:5,3,6。由於3,5,6已標記為T,不走。
4無路可走,返回上一個分支6。6有兩個鄰居點:0,4。由於0,4已標記為T,不走。
6無路可走,返回上一個分支0。0有四個鄰居點:5,1,2,6。由於5,2,6已標記為T,不走。
0走向1:1標記為True;1從0來,故EdgeTo[1]=0; 1與0屬於同一組:0組。
1有一個鄰居點:0。由於0已標記為T,不走。返回上一個分支0。
0有四個鄰居點:5,1,2,6。由於5,2,6,1已標記為T,不走。
0無路可走,且無上一個分支點。查找標記為F的其它點,隨便選一個來走,如7。
7標記為True;7屬於組1。
然后重復上述過程,直到所有點標記為T為止。
看懂上述例子的思路,那么通用思路也是一樣的。如果想查1和6是否相連,只需看它們所屬組別是否相同即可。
如查1到6的路徑,從1和6分別查EdgeTo,查到重復點,則把路線結合即可。
代碼大概是這樣的:
4. 廣度優先搜索(breadth-first search)
從深度優先搜索的思路上看,顯然不適合用於尋找最短路徑。
這里介紹另一種思路:廣度優先搜索。此算法需要用到隊列(queue),對隊列不熟悉的,可以先看下隊列。
從例子入手:
為了方便講解,作下圖:
Marked和EdgeTo與深度優先搜索的一樣,DistTo表示起始點與目標點的距離。如:0與3的距離為2,即DistTo[3]=2。
先看最大的這組:
從0開始,先把0標記了(變紅)。記0到0的距離為0。(從哪個點開始可以根據需求來決定。)把0加入到隊列A中。
0的鄰居點有4個:5,1,2,6。隊列A輸出一個值:0,隊列A按順序輸入值5,1,2,6。(這個順序沒所謂,1,6,5,2也行。)
5,1,2,6全部標記為True,從0點來,距離0點為1:
隊列A輸出一個值,如果上一步是按5,1,2,6順序輸入的,則這里輸出的是5.
5的鄰居點有3個:0,3,4。由於0已經標記為True,所以不管。將3,4按順序輸入到隊列A中(當然,順序無所謂)。
3,4全部標記為True,從5點來,距離0點為2:
隊列A輸出一個值,如果之前那步是按5,1,2,6順序輸入的,則這里輸出的是1.
1的鄰居點有1個:0。由於0已經標記為True,所以不管。
隊列A輸出一個值,如果之前那步是按5,1,2,6順序輸入的,則這里輸出的是2.
2的鄰居點有1個:0。由於0已經標記為True,所以不管。
隊列A輸出一個值,如果之前那步是按5,1,2,6順序輸入的,則這里輸出的是6.
6的鄰居點有2個:0,4。由於0,4已經標記為True,所以不管。
隊列A輸出一個值,如果上一步是按3,4順序輸入的,則這里輸出的是3.
3的鄰居點有2個:5,4。由於5,4已經標記為True,所以不管。
隊列A輸出一個值,如果上一步是按3,4順序輸入的,則這里輸出的是4.
4的鄰居點有3個:5,3,6。由於3,5,6已經標記為True,所以不管。
隊列A為空,這部分處理完畢。
其它部分也是相同處理方法,DistTo要小心處理,一般要遍歷全部的時候,DistTo是不需要的。DistTo一般用於尋找兩個點之間的最短距離與路線。
當然也可以像深度優先處理那樣加入Group來記錄哪些點在哪個團體中。
通用思路可以從例子中看出來,實在看不出的,可以看下面的代碼。
代碼大概是這樣的: