深度優先搜索(depth-first search)是對先序遍歷(preorder traversal)的推廣。”深度優先搜索“,顧名思義就是盡可能深的搜索一個圖。想象你是身處一個迷宮的入口,迷宮中的路每一個拐點有一盞燈是亮着的,你的任務是將所有燈熄滅,按照DFS的做法如下:
1. 熄滅你當前所在的拐點的燈
2. 任選一條路向前(深處)走,每經過一個拐點將燈熄滅直到與之相鄰的拐點的燈全部熄滅后,原路返回到某個拐點的相鄰拐點燈是亮着的,走到燈亮的拐點,重復執行步驟1
3. 當所有燈熄滅時,結束
將上面的例子抽象出來的DFS的算法描述(C偽代碼)如下:
//布爾型數組Visited[]初始化成false
void DFS(Vetex v) { Visited[v] = true; for each w adjacent to v if (!Visited[w]) DFS(w); }
可以看出上述的DFS為遞歸算法,可以利用棧將其轉為非遞歸。C偽代碼如下:
//布爾型數組Visited[]初始化成false
void DFS(Vertex v) { Visited[v] = true;
Stack sta = MakeStack(MAX_SIZE); Push(sta, v); while (!Empty(sta)) { Vertex w = Pop(sta); for each u adjacent to w { if (!Visited[u]) { Push(sta, u); Visited[u] = true;
} } } }
引理: 若圖G是連通的,則通過深度優先搜索可以對它的所有頂點進行標記,並且在算法的執行過程中,它的每一條邊至少被查看過一次。
證明: 假設結論不成立,令U表示算法最終未被標記過的頂點的集合。由於G是連通的,因此在U中至少有一個頂點與一個被標記過的頂點相連。但是這種情況不可能成立,因為一旦一個頂點被訪問過了,則所有與它相連的未被標記過的頂點也會被訪問(從而也被標記)。故所有頂點都會被訪問而標記,並且一旦某個頂點被訪問,它相連的邊就會被查看,所以每條邊都將被查看過。
然而,如果一個圖G不是連通的,要標記所有頂點,需對DFS稍作修改:若在第一次嘗試所有頂點都被標記過,則圖是連通的,否則,從任意一個未被標記的頂點開始,再次執行DFS。所以我們可以利用DFS確定一個圖是否連通。C偽代碼描述上述算法如下:
/*返回連通成分的數目*/
int ConnectedComponents ( Graph G ) { int componentNum = 0; for ( each v in G ) if ( !visited[V] ) { DFS( v ); componentNum += 1; } return componentNum; }
上述算法的復雜度:
若有N個頂點、 E條邊,時間復雜度是
用鄰接表存儲圖,有O(N+E)
用鄰接矩陣存儲圖,有O(N^2)
深度優先搜索的相關練習:
06-圖2 Saving James Bond - Easy Version
拓展閱讀:
參考資料:
《數據結構與算法分析-C語言描述》
《算法引論》