1. 有向圖(Directed Graphs)
有向圖與無向圖是很像的,如果對無向圖不熟悉,建議先看一下無向圖。
在討論有向圖的算法前,先討論如何構建有向圖。
構建有向圖的方法基本與無向圖的方法一模一樣。
首先,有向圖是長這樣的:
也是有兩個關鍵點:
a. 這個有向圖有哪些點
b. 哪些點可以通往哪些點(箭頭代表可通往的方向,如此例子中,0可以去1,但1不可以去0。)
構建有向圖也是用鄰接矩陣(Adjacency-matrix)或鄰接列表(Adjacency-list)。
這個矩陣和列表也和無向圖的基本一樣,唯一的區別在於,有向圖的矩陣不是關於對角線對稱的。
有向圖的鄰接列表顯示:(0可以去的點有5,1,即adj[0]=5,1)
下面開始討論有向圖的算法:深度優先搜索(depth-first search)和廣度優先搜索(breadth-first search)。
無向圖的Group在有向圖中不適用,因為有些路是單方向的。稍后我們將引入強聯系(Strong Components)的概念來解決Group的問題。
2. 深度優先搜索(depth-first search)
有向圖的深度優先搜索與無向圖的深度優先搜索很像,像到什么程度呢?甚至可以直接把無向圖的深度優先搜索代碼直接復制過來用。
看例子:
0~12代表着圖中的所有點。
一開始所有點標記為False(F),當我們走到某個點后,此點標記為True(T)。標記過的點不需要再走一次。
EdgeTo記錄了部分路線,所有部分路線可以整合成一個完整的路線。例如從E點抵達A點,則記為EdgeTo[A]=E;
從0開始,先把0標記為True(變紅)。
然后0可以去的點有5,1,這些點都還沒標記為True,隨便選一個:5。
5標記為True。5從0來,故EdgeTo[5]=0;
5只有一個點可以去:4。4還沒標記為True,去4。
4標記為True。4從5來,故EdgeTo[4]=5。
4可以去的點有3,2,這些點都還沒標記為True,隨便選一個:3。
3標記為True。3從4來,故EdgeTo[3]=4。
3可以去的點有5,2,5已標記,不管。2沒標記,只能去2。
2標記為True。2從3來,故EdgeTo[2]=3。
2可以去的點有0,0已標記,不管。2無路可走,返回上一個分支3。
3無路可走,返回上一個分支4。
4無路可走,返回上一個分支5。
5無路可走,返回上一個分支0。
0還有一個點可以去:1。
1標記為True。1從0來,故EdgeTo[1]=0。
1無路可走,返回上一個分支0。
0無路可走,且無上一個分支,此部分結束。
查找標記為F的其它點,隨便選一個來走,如7。
重復上述過程,直到所有點標記為T為止。
通用思路也呼之欲出了,見代碼:
3. 廣度優先搜索(breadth-first search)
有向圖的深度優先搜索與無向圖的深度優先搜索基本一樣。具體詳細內容可以去無向圖那里去看,這里講的會比較快。
新建隊列A。
從0出發(從哪個點開始可以根據需求來決定。),0先標紅。0進隊列A。
隊列A輸出一個值:0。
0可以去的點有1,5。1,5全部標紅,1,5輸進隊列A。
隊列A輸出一個值:1。1沒有可以去的點,不管。
隊列A輸出一個值:5。5可以去的點有4,4標紅,4進隊列A。
隊列A輸出一個值:4。4可以去的點有2,3;2,3標紅,2,3進隊列A。
隊列A輸出一個值:2。2可以去0,3,但它們都是已標記的,不管。
隊列A輸出一個值:3。3可以去2,5,但它們都是已標記的,不管。
隊列A為空,此部分處理完成。
其它部分也是相同處理方法,DistTo要小心處理,一般要遍歷全部的時候,DistTo是不需要的。DistTo一般用於尋找兩個點之間的最短距離與路線。
代碼與無向圖的一樣:
4. 拓撲排序(Topological Sort)
一、什么是拓撲排序?
先從一個例子中直觀地感受一下:左圖是有向圖,右圖是這個有向圖拓撲排序后形成的拓撲序列。(當然,這個拓撲序列是豎着的還是橫着的都沒所謂,怎么好看怎么來。)
在現實生活中,很多任務是有先決條件的,例如沖一杯咖啡,我們需要先做4個前提任務:
a.水燒開
b.把開水倒入杯子中
c.把咖啡沖劑倒入杯子中
d.把杯子里面的東西攪勻
b和c的順序沒所謂,a要在b之前完成,d要在a,b,c之后才能進行。如果把這些任務拓撲排序:
圖中e是喝咖啡。
由上圖可知,要完成e需先完成d;要完成d需先完成b和c;要完成b需先完成a。
像這樣,拓撲序列可以把一個大任務分成若干小任務,這是做大型項目所需要的,並且任務流程十分清晰。
拓撲序列也可以在許多地方有所作為,這里不一一列出,有興趣的可自行搜索。
下面將介紹如何對一個有向圖進行拓撲排序。
二、如何進行拓撲排序?
一般來說,我們只會對有向無環圖(DAG, Directed Acyclic Graph)進行拓撲排序,這里的無環是指無內部循環。
我們舉一個有內部循環的例子:
此例子中,要完成5需要先完成3;要完成3需要先完成4;要完成4需要先完成5。
試想下,如果現實生活中有個任務是3,4,5這種結構的,那么這個任務如何完成?
但是,如果對這個有向有環圖進行拓撲排序,會有什么效果?答案就是會把這個循環的部分(強聯系體)看成一個點,然后這個點與其它沒循環的點形成拓撲序列。這個將在下面介紹強聯系體的時候講到。
接下來,我們來看一下如何對有向無環圖進行拓撲排序,需要用到棧(Stack),不熟悉的,建議先去看一下棧。
記住,棧是后進先出的。拓撲排序就是進行一次深度優先搜索。
從例子入手:創建棧A
從0出發,0有3個可以去的點:2,5,1;隨便選一個:2:
2沒有可以去的點,把2加進棧A,返回上一個分支0;
0還有兩個可以去的點,隨便選一個:5;
5可以去2,但2已標記,不管;把5加進棧A,返回上一個分支0;
0還有一個可以去的點:1;去1;
1可以去4;去4;
4無路可去,加進加進棧A,返回上一個分支1;
1無路可去,加進加進棧A,返回上一個分支0;
0無路可去,加進棧A,無上一個分支,去找其它未被標記的值,隨便選一個:6;
6可以去的點都標記了,無路可去且無上一個分支,加進棧A,去找其它未被標記的值:3
3可以去的點都標記了,無路可去且無上一個分支,加進棧A,沒其它未被標記的值,搜索結束。
把棧的值逐一輸出(后進先出!),得到拓撲序列:3,6,0,1,4,5,2。你或許會發現這個圖與一開始給的不一樣。其實這個區別就是先把開水倒入杯子中還是先把咖啡沖劑倒入杯子中的區別。本質上是一樣的。
總結一下通用思路就是:對一個有向無環圖進行一次深度優先搜索。把無路可去的點依次加入到棧中,搜索結束后,把棧的點逐一輸出,得到拓撲序列。
可以想到,代碼只是在深度優先搜索的代碼中加入少量代碼:
5. 強聯系(Strong Components)
一、什么是強聯系?
上文提及,有向圖的強聯系是與無向圖的組別(Group)相對應的。
如果一堆點中,點A可以去點B(間接或直接),且點B可以去點A(間接或直接),則點A與點B是強聯系。
例如:
這個圖中,0,2,3,4,5互為強聯系,這些強聯系形成了一個強聯系體。要想快速知道給定的兩個點是否是強聯系,只需檢查它們是否同屬一個強聯系體即可。
二、如何把一個有向圖划分成數個強聯系體?
這個問題困擾了眾多算法研究者多年,這里將介紹一種相對簡單的算法:Kosaraju-Sharir算法(也稱Kosaraju算法)
此算法由S. Rao Kosaraju在19世紀80年代提出。
我們將進行兩次深度優先搜索,第一次是把有向圖的所有方向反過來,然后進行拓撲排序,得到一個拓撲序列。
然后根據這個拓撲序列,按原來的有向圖的方向進行深度優先搜索,並得出強聯系體。
從例子入手:
先把有向圖反過來:
從0開始,0有兩個可以去點2,6;隨便選一個:6
6有兩個可以去的點8,7;隨便選一個:7
7無路可走,加入棧A中,返回上一個分支點6。
6還可以去8;
8可以去6,但6已經標記,不管;8無路可走,加入棧A,返回上一個分支點6。
6無路可走,加入棧A,返回上一個分支點0。
0還可以去2;
2兩個可以去的點3,4;隨便選一個:3;
3可以去2,4,但2以標記,不去;去4;
4有三個可以去的點5,6,11;6已標記,不去;剩下的隨便選一個:5;
5可以去的點都標記了,無路可走,加入棧A,返回上一個分支點4。
4還可去11,去11;
11可以去9,去9;
9可以去12,去12;
12可以去11,10,但11已標記,不去,故去10;
10可以去的點都標記了,無路可走,加入棧A,返回上一個分支點12。
12可以去的點都標記了,無路可走,加入棧A,返回上一個分支點9。
9可以去的點都標記了,無路可走,加入棧A,返回上一個分支點11。
11可以去的點都標記了,無路可走,加入棧A,返回上一個分支點4。
4可以去的點都標記了,無路可走,加入棧A,返回上一個分支點3;
3可以去的點都標記了,無路可走,加入棧A,返回上一個分支點2。
2可以去的點都標記了,無路可走,加入棧A,返回上一個分支點0。
0可以去的點都標記了,無路可走,加入棧A,無上一個分支點,找還沒標記的點1,去1。
1可以去的點都標記了,無路可走,加入棧A,無上一個分支點,無還沒標記的點,搜索結束。
棧逐一輸出數值,得到反拓撲序列(因為是反的有向圖):1,0,2,3,4,11,9,12,10,5,6,8,7
當有向圖有內部循環(即強聯系體)時,這個拓撲序列怎么理解?見下圖
每個強聯系體可以看作是一個整體,然后所有整體形成拓撲序列。
然后我們將根據這個拓撲序列,按原來的有向圖的方向進行深度優先搜索:
根據反拓撲序列,從1開始;
1無路可去,屬於強聯系體0;
根據反拓撲序列順序尋找未被標記的點,下一個去0,0屬於強聯系體1,0可以去1,5,但1已標記,不去,故去5;
5屬於強聯系體1,可以去4,去4;
4屬於強聯系體1,可以去2,3,隨便選一個:2;
2屬於強聯系體1,可以去0,3,但0已標記,不去,故去3;
3屬於強聯系體1,可以去的點都標記了,無路可走,返回上一個分支點2。
2可以去的點都標記了,無路可走,返回上一個分支點4。
4可以去的點都標記了,無路可走,返回上一個分支點5。
5可以去的點都標記了,無路可走,返回上一個分支點0。
0可以去的點都標記了,無路可走,無上一個分支點,根據反拓撲序列順序尋找未被標記的點,下一個去11;
11屬於強聯系體2,可以去12,4,但4已標記,不去,故去12;
12屬於強聯系體2,可以去9,去9;
9屬於強聯系體2,可以去10,11,但11已標記,不去,故去10;
10屬於強聯系體2,可以去的點都標記了,無路可走,返回上一個分支點9。
9可以去的點都標記了,無路可走,返回上一個分支點12。
12可以去的點都標記了,無路可走,返回上一個分支點11。
11可以去的點都標記了,無路可走,無上一個分支點,根據反拓撲序列順序尋找未被標記的點,下一個去6;
6屬於強聯系體3,可以去8,7,隨便選一個:8;
8屬於強聯系體3,無路可走,返回上一個分支點6。
6可以去的點都標記了,無路可走,無上一個分支點,根據反拓撲序列順序尋找未被標記的點,下一個去7;
7屬於強聯系體4,無路可走,無上一個分支點,沒有其它未被標記的點,結束搜索。
就這樣,划分完畢。
看懂了思路,代碼實現應該不難,下面有一份現成的可供參考。
實現代碼: