Graph: Depth-First Search(DFS,深度優先搜尋)
文章推薦指數: 80 %
演算法
Togglenavigation
SecondRound
Archives
Tags
About
先備知識與注意事項
在BinaryTree:Traversal(尋訪)中介紹過Pre-OrderTraversal,其Visiting順序:「Current(V)-left(L)-right(R)」可以解讀成「先遇到的node就先Visiting」,因此,每一組「Current-left-right」必定是CurrentNode先Visiting,接著是leftchild,最後才是rightchild。
若對圖一中的BinaryTree進行Pre-OrderTraversal,定義Visiting為印出(print)資料,將得到:
ABDEGHCF
圖一。
Depth-FirstSearch(DFS,深度優先搜尋)的核心精神便如同Pre-OrderTraversal:「先遇到的vertex就先Visiting」,並且以先遇到的vertex作為新的搜尋起點,直到所有「有edge相連的vertex」都被探索過。
以圖二的迷宮為例,把迷宮矩陣中的每一格定義成一個vertex,若兩個vertex之間有路,則建立edge相連。
若要在迷宮中尋找抵達終點的路線,通常會先選擇其中一條路線,只要有路就繼續往前走。
有可能一路走到終點,也有可能遇到死路。
若遇到死路則回到上一個岔路,往另一條路探索路線。
依此類推,直到走出迷宮。
圖二:迷宮問題(mazeproblem)。
目錄
Depth-FirstSearch(DFS,深度優先搜尋)
演算法
程式碼
討論
Depth-FirstTree
4種edge
參考資料
BFS/DFS系列文章
Depth-FirstSearch(DFS,深度優先搜尋)
考慮圖三(a)的Graph(沒有weight的directedgraph):
圖三(a):。
若以vertex(A)為起點進行DFS(),可以得到:
若Graph中的vertex與vertex(A)之間存在至少一條path,則DFS()必定能找到其中一條path從vertex(A)抵達該vertex。
但是這條path未必保證是最短路徑(shortestpath)。
看起來好像沒有BFS()這麼殺手級,雖然找到一條路卻不保證是最短路徑。
但其實DFS()還是很有用的,因為經過一次DFS()後,將得到一項資料稱作finish,而finish竟然可以用來...!?
別轉台,看下去。
演算法
以下介紹的DFS()需要資料項目共有:
time:在整個DFS()的過程會有一條「時間軸」,若Graph中有\(N\)個vertex,「時間軸」上一共會有\(2N\)個「時間點」。
discover與finisharray:每個vertex會被標記上兩個「時間點」,分別是「被發現(discover)」的時間與「結束(finish)」的時間:
discover:例如,vertex(B)被vertex(A)找到,則discover[B]會是discover[A]加一,表示vertex(B)在整個時間軸上是在vertex(A)之後被找到(這其中便存在「ancestor-descendant」的關係)。
finish:若vertex(B)已經經由有效edge探索過所有與之相連的vertex,表示以vertex(B)為起點的探索已經結束,便標上finish[B]。
colorarray:利用color標記哪些vertex已經「被發現」與「結束」。
白色表示該vertex還沒有「被發現」;
灰色表示該vertex已經「被發現」,但是還沒有「結束」。
黑色表示該vertex已經「結束」。
predecessorarray:記錄某個vertex是被哪一個vertex找到的,如此便能回溯路徑(如同BFS(),DFS()亦能生成一個PredecessorSubgraph)。
DFS()的方法如下:
初始化(initialization),見圖三(b):
先把time設為0,表示還沒有任何vertex「被發現」。
把所有vertex塗成白色。
把所有vertex的predecessor清除(或者設成NULL、-1,視資料形態(datatype)而定)。
把所有vertex的discover與finish設成0,表示還沒有開始進行DFS()。
圖三(b):。
以vertex(A)作為起點,見圖三(c):
將vertex(A)塗成灰色,表示已經「被發現」。
由於vertex(A)已經「被發現」,便把discover[A]設為++time。
原本time=0,而vertex(A)是DFS()的起點,所以++time之後distance[A]=1便表示vertex(A)是第一個被發現。
接著尋找所有與vertex(A)相連之vertex,只要遇到第一個仍為白色的vertex,便把該vertex設為新的搜尋起點,並將該vertex之predecessor設為vertex(A)。
圖三(a)之AdjacencyList中,第一個與vertex(A)相連的vertex為vertex(B),接著便以vertex(B)為起點,繼續尋找與vertex(B)相連之「最近的」vertex。
由於從vertex(A)找到了vertex(B),便表示「從vertex(A)出發的path」還在更新,於是vertex(A)便還沒有「結束」,所以finish[A]不需要更新。
那麼,什麼時候會更新finish[A]呢?
在AdjacencyList中,與vertex(A)相連之vertex有vertex(B)與vertex(C),要在這兩個vertex都「作為搜尋起點」,並且「探索完所有相連的vertex」後(也就是更新完finish[B]與finish[C]後),才會更新到finish[A]。
圖三(c)中的「TimeStamp(時間戳記)」即為「時間軸」。
此時進入「剛發現vertex(A)」,並且尚未結束「以vertex(A)作為搜尋起點」的階段。
圖三(c):。
從vertex(A)作為探索起點,「最先發現」的是vertex(B),便以其作為新的起點:
把vertex(B)塗成灰色,表示已經「被發現」。
由於vertex(B)已經「被發現」,便把discover[B]設為++time,也就是distance[B]=2。
接著,找到AdjacencyList中第一個與vertex(B)相連,且為白色的vertex(D),將vertex(D)視為新的搜尋起點。
此時圖三(d)的「時間軸」表示,「以vertex(A)作為起點」之搜尋尚未結束(vertex(A)還沒有被塗黑),而且「以vertex(B)作為起點」之搜尋剛剛開始。
圖三(d):。
來到「以vertex(D)作為起點」之搜尋,大致上與「以vertex(B)作為起點」之搜尋相同:
把vertex(D)塗成灰色,表示已經「被發現」。
由於vertex(D)已經「被發現」,便把discover[D]設為++time,也就是distance[D]=3。
接著,找到AdjacencyList中第一個與vertex(D)相連,且為白色的vertex(E),將vertex(E)視為新的搜尋起點。
此時圖三(e)的「時間軸」表示,「以vertex(A)與vertex(B)作為起點」之搜尋都還沒結束(vertex(A)、vertex(B)還沒有被塗黑),而且「以vertex(D)作為起點」之搜尋剛剛開始。
圖三(e):。
來到「以vertex(E)作為起點」之搜尋,與前兩個步驟相同,不再贅述,見圖三(f)。
圖三(f):。
關鍵是圖三(g)。
當vertex(E)再也找不到任何「能夠抵達、且為白色」的vertex時(見圖三(g)中Graph的箭頭方向,並對照圖三(a)之AdjacencyList),就表示「以vertex(E)作為起點」之搜尋已經「結束」,此時:
把vertex(E)塗成黑色,表示「以vertex(E)作為起點」之搜尋已經「結束」。
把finish[E]設成++time。
原先在vertex(E)「被發現」時time=4,更新後,表示vertex(E)之搜尋在time=5時「結束」。
當vertex(E)「結束」後,便回到vertex(E)的predecessor,也就是「發現vertex(E)」的vertex(D),繼續探索其他vertex(D)能夠走到的vertex。
此時,圖三(g)的「時間軸」表示,discover[E]=4,finish[E]=5,往後搜尋過程就不會再有vertex(E)出現。
而以vertex(A)、vertex(B)、vertex(D)作為起點的搜尋則持續進行。
圖三(g):。
接著,從vertex(E)退回到vertex(D),並從vertex(D)繼續發現vertex(F)還是白色,於是便以「vertex(F)作為起點」進行搜尋:
把vertex(F)塗成灰色。
把discover[F]設為++time。
繼續尋找所有從vertex(F)能夠走到,且為白色的vertex。
圖三(h):。
觀察圖三(i)的Graph與圖三(a)的AdjacencyList,發現從vertex(F)能夠走到vertex(B),但是由於vertex(B)已經是「灰色」,表示「以vertex(B)為起點」之搜尋已經開始且尚未結束,於是vertex(F)無法「發現」vertex(B),也無法走到其餘vertex,所以,vertex(F)便宣告「結束」:
把vertex(F)塗成黑色,表示「以vertex(F)作為起點」之搜尋已經「結束」。
把finish[F]設成++time。
當vertex(F)「結束」後,便回到vertex(F)的predecessor,也就是「發現vertex(F)」的vertex(D),繼續探索其他vertex(D)能夠走到的vertex。
圖三(i):。
如圖三(j),所有vertex(D)能夠抵達的vertex都已經變成黑色,於是「以vertex(D)作為起點」之搜尋便結束:
把vertex(D)塗成黑色,表示「以vertex(D)作為起點」之搜尋已經「結束」。
把finish[D]設成++time。
當vertex(D)「結束」後,便回到vertex(D)的predecessor,也就是「發現vertex(D)」的vertex(B),繼續探索其他vertex(B)能夠走到的vertex。
圖三(j):。
接著便以上述邏輯重複步驟,直到vertex(A)被塗成黑色,見圖三(k)-(n)。
圖三(k):。
圖三(l):。
圖三(m):。
圖三(n):。
在「以vertex(A)為起點」之搜尋結束後,必須確認Graph中還有沒有白色的vertex,如圖三(n),Graph裡還有vertex(G)與vertex(H)仍然是白色,因此挑選其中一個vertex作為新的起點。
這裡是按照AdjacencyList的順序,由於vertex(G)在vertex(H)之前,便先挑選vertex(G),接著重複上述步驟,見圖三(o)-(r)。
圖三(o):。
圖三(p):。
圖三(q):。
圖三(r):。
當Graph中所有vertex都被塗成黑色,便完成DFS()。
程式碼
由以上說明可以觀察出,DFS()本質上是一種「遞迴(recursion)結構」,而遞迴結構其實是利用了系統的「堆疊(stack)」,因此,這兩種方式皆能實現DFS(),以下提供的範例程式碼將以遞迴形式完成。
如同上一篇Graph:Breadth-FirstSearch(BFS,廣度優先搜尋),以下將使用int處理資料,把\(9\)個vertexcharA~I依序對應到int0~8。
範例程式碼包含兩個部分main()與classGraph。
在main()中,主要兩件事情:
建立如圖三(a)的AdjacencyList;
進行DFS()。
在classGraph中:
privatemember:
num_vertex:需要在定義Graph的object(物件)時,給定vertex的數目,以便建立AdjacencyList(或者AdjacencyMatrix)。
std::vector<:list>>AdjList:利用C++標準函式庫(STL)提供的container(容器):std::vector與std::list來實現。
color、discover、finish、predecessor:將在DFS()中使用,功能如上述。
publicmember:
Constructor:Graph(intnum_vertex):在定義Graph的object(物件)時,需要知道vertex的數目,並在constructor中定義好AdjList。
AddEdgeList(intfrom,into):功能便是在AdjList新增從from到to的edge。
DFS(intStart):需要知道起點vertex。
DFSVisit(intvertex,int&time):利用遞迴函式呼叫,進行color、discover、finish與predecessor等資料更新的主要函式。
//C++code
#include
#include
分類出四種edge除了可以作為大腦體操,還可以根據Graph中是否具有/不具有某些edge來區分Graph的性質:
若在undirectedgraph上執行一次DFS(),所有的edge不是Treeedge就是Backedge。
若在directedgraph上執行一次DFS(),沒有產生Backedge,則此directedgraph必定是acyclic(沒有迴路)。
諸如此類的,是不是很有趣呢?
(好像也還好。
)
最後再看一次DFS()的流程,見圖七:
圖七:。
以上便是Depth-FirstSearch(DFS,深度優先搜尋)的介紹。
在接下來的文章中,將利用DFS()與神奇的finish:
進行TopologicalSort(拓撲排序)。
找到directedgraph中的StronglyConnectedComponent(SCC)。
不過在這之前,會先介紹利用BFS()與DFS()找到undirectedgraph中的connectedcomponent作為暖身。
參考資料:
IntroductiontoAlgorithms,Ch22
FundamentalsofDataStructuresinC++,Ch6
CodeReview:DepthFirstSearchandBreadthFirstSearchinC++
BFS/DFS系列文章
Graph:Breadth-FirstSearch(BFS,廣度優先搜尋)
Graph:Depth-FirstSearch(DFS,深度優先搜尋)
Graph:利用DFS和BFS尋找ConnectedComponent
Graph:利用DFS尋找StronglyConnectedComponent(SCC)
Graph:利用DFS尋找DAG的TopologicalSort(拓撲排序)
回到目錄:
目錄:演算法與資料結構
tags:C++,Graph,DFS,
延伸文章資訊
- 1Depth-first search 深度優先搜尋法
Depth-first search (DFS) is an algorithm for traversing or searching a tree, ... 深度優先搜尋法,是一種用來遍尋一...
- 2實作Graph與DFS、BFS圖形走訪演算法 - 寫點科普
DFS 就像試探著走迷宮,從起點開始、任意選一點與起點相鄰的點行走,行走過的點會被標記起來;再將下一個點視為起點、繼續選擇與該點相鄰的點行走…
- 3【筆記】DFS (Depth First Search,深度優先搜尋) - Yui Huang ...
【用途】用來遍歷樹(tree)或圖(graph)的演算法。 【觀念】由圖的某一點開始搜尋,先探尋鄰接邊(edge)上未搜尋的一點,並儘可能往深處搜索,直到最後, ...
- 4[演算法] [C++ / Python] 深度優先搜尋Depth-First-Search - Part I
更新:熱騰騰的Part II 出爐囉! 深度優先搜尋,Depth-First-Search,簡稱DFS,是一種用於圖或樹的遍歷、搜尋演算法。 樹. 我們先畫一棵樹如下:.
- 5深度優先搜尋(DFS)和廣度優先搜尋(BFS)演算法 - MagicLen