1. 程式人生 > >廣度優先演算法,深度優先演算法和DijKstra演算法

廣度優先演算法,深度優先演算法和DijKstra演算法

我們經常會碰到最短路徑問題,而最短路徑問題的解決方法多種多樣,廣度優先搜尋(BFS),深度優先搜尋(DFS)和DijKstra演算法貌似都能解決這個問題,這裡就簡單介紹一下這些演算法,分析一下它們的適用範圍。

一、原理剖析:
1 廣度優先搜尋(BFS)
廣度優先搜尋依賴的是佇列解決問題。佇列中的每一個節點需要包含記錄以下內容:該節點到起點的距離dist,該節點的前驅節點past,該節點在當前路徑下是否被訪問過visit(0表示沒有訪問過,1表示當前路徑下正在訪問,2表示該節點周圍的所有節點都已經被訪問過)。
程式設計邏輯大致如下:

初始化:
起點值初始化(past=NULL,dist=0
,visit=1) 其他節點值初始化(past=NULL,dist=無窮,visit=0) 起點入隊 迴圈1:直到佇列中沒有元素 從隊伍中輸出一個節點作為當前節點 迴圈2:訪問與當前節點連通但是【沒有被訪問過】的節點(visit=0的節點) 將即將訪問的節點記為正在訪問的狀態 將即將訪問的節點的狀態更新(past=當前節點,dist=即將訪問的節點到當前節點的距離,visit=1) 即將訪問的節點入隊 將當前節點的visit記為2(因為與它連線的所有節點都被訪問過)

為什麼需要記錄這些內容?我們逐一解釋:首先,節點到起點的距離很好解釋,這是問題的需求,但是在二叉樹的層序遍歷

時,我們僅僅需要將節點輸出,而不需要計算距離,此時不記錄這個內容也沒關係。如果題意不要求輸出最短的路徑,而是隻要求我們記錄最短的路徑是多少,那不記錄前驅節點問題也不大,它不影響最短路徑的求解。記錄該節點在當前路徑下是否被訪問的目的是,避免圖中有環路而造成節點的重複訪問和死迴圈,但是對於樹形結構,當前節點不可能遍歷以前訪問過的節點,這個內容可以不記錄。綜上,在寫廣度優先演算法的程式碼時,要依據需求變通,不應教條。

2 深度優先搜尋(DFS)
深度優先搜尋依賴的是遞迴,你完全可以把深度優先搜尋理解為動態規劃的一種形式。它一樣要記錄:該節點到起點的距離dist,該節點的前驅節點past,該節點在當前路徑下是否被訪問過visit(0表示沒有訪問過,1表示當前路徑下正在訪問,2表示該節點周圍的所有節點都已經被訪問過)。記錄這些值的目的與廣度優先搜尋也差不多。
程式設計邏輯大致如下:

初始化:
所有節點值初始化(past=NULL,dist=無窮,visit=0)
遞迴DFS(當前節點)
  當前路徑正在訪問當前節點(visit=1)
  對與當前節點連通的所有【沒有被訪問過】的節點
      改變即將訪問的節點的狀態(past為當前節點,dist為即將訪問的節點到當前節點的距離)
      DFS(即將訪問的節點)
      【如果有環路,這裡還要加一步:如果當前節點的visit不是2,就把visit設為0,否則被訪問過一次就再也訪問不了了】
  將當前節點的visit記為2(因為與它連線的所有節點都被訪問過) 

3 DijKstra演算法
DijKstra演算法是運籌學中求最短路徑的常規演算法,它的中心思想是:讓每個節點記錄它到起點的最短路徑,其實也可以理解為動態規劃的一種形式。與前面兩種方法相同,DijKstra演算法也需要記錄:該節點到起點的距離dist,該節點的前驅節點past。
但是不同的是,不需要記錄該節點是否被訪問過,而是記錄:該節點到起點的最短距離是否已經確定visit(0表示還沒有確定了該節點到起點的最短距離,1表示已經確定該節點到起點的最短距離)。
程式設計邏輯大致如下:

初始化:
起點初始化(dist=0,past=NULL,visit=1)
其他節點初始化(dist=無窮,past=起點,visit=0)
迴圈:對於所有節點
  迴圈:對於所有【不確定到起點最短距離】的節點,找出距離起點最近的節點並記錄距離
  更新找出的節點的狀態(visit=1)
  迴圈:對於所有【不確定到起點最短距離】的節點
    計算它到剛才找出的節點的距離
    如果節點經過剛才找出的節點到起點的距離小於節點直接到達起點的距離
      更新節點的狀態(dist=找出的節點的dist+找出的節點到該節點的距離,past=找出的節點)

這裡寫圖片描述

這裡寫圖片描述

二、優缺點剖析:
首先我們看如下無向圖:假設所有邊的權重都是正數
這裡寫圖片描述
如果我想求從1到5的最短距離,上述描述的三種方法都可以。

我們再看如下情況:
這裡寫圖片描述
假設所有邊的權重都是相同的正數,求從1到3的最短路徑。
我們利用DFS依照節點的id從小到大搜索,只能得到1-2-3這樣的結果,那是因為1-3這條路徑根本沒有被訪問到

我們再看演算法導論一書中給出的BFS和DFS示意圖:
這裡寫圖片描述
上圖是BFS的示意圖,從圖中可以清楚地看出一次BFS的完整過程。我們可以發現:沒有加粗的邊是沒有被訪問過的。
這裡寫圖片描述
上圖是DFS的示意圖,從圖中可以清楚地看出一次DFS的完整過程,我們可以發現:虛線邊是沒有被訪問過的。

到這裡結論已經很清楚了:
DFS和BFS是以遍歷所有節點為主要目的的演算法,他們不一定能遍歷所有的路徑。即使在一些三種方法通用的情況下,DFS和BFS還有一些程式設計上的繁瑣之處,主要表現在,我們需要額外定義變數存放最短路徑,節點自身攜帶的最短路徑和前驅節點是隨著迴圈/迭代的進行不斷更新的,它不能在節點中保留最優解。而對於DijKstra演算法而言,節點自身保留的就是最優情況,不需要額外定義變數。

綜上所述:我的建議是,針對樹形結構求解類似最短路徑或最小權重和的題目時,DFS和BFS是不錯的選擇,但是當遇到有環狀結構的最短路徑題時應該格外警惕,此時選擇DijKstra演算法比較穩妥。

應用分割線:
如果您看到這裡,希望在留言裡留下您見到的DFS,BFS,DijKstra演算法在具體問題中的應用,我看到後會更新在這裡。
廣度優先BFS的應用:
二叉樹的層序遍歷

深度優先DFS的應用:
樹的深度求解

DijKstra:
最短路徑