深度優先搜索(DFS)

深度優先搜索(DFS)

算法入門】

郭志偉@SYSUraphealguo(at)qq.com

2012/05/12

1.前言

深度優先搜索(縮寫DFS)有點類似廣度優先搜索,也是對一個連通圖進行遍歷的算法。它的思想是從一個頂點V0開始,沿着一條路一直走到底,如果發現不能到達目標解,那就返回到上一個節點,然後從另一條路開始走到底,這種儘量往深處走的概念即是深度優先的概念。

你可以跳過第二節先看第三節,:)

2.深度優先搜索VS廣度優先搜索


2.1演示深度優先搜索的過程

還是引用上篇文章的樣例圖,起點仍然是V0,我們修改一下題目意思,只需要讓你找出一條V0到V6的道路,而無需最短路。


2-1 尋找V0V6的一條路(無需最短路徑)

假設按照以下的順序來搜索:

1.V0->V1->V4,此時到底盡頭,仍然到不了V6,於是原路返回到V1去搜索其他路徑;

2.返回到V1後既搜索V2,於是搜索路徑是V0->V1->V2->V6,,找到目標節點,返回有解。

這樣搜索只是2步就到達了,但是如果用BFS的話就需要多幾步。

2.2深度與廣度的比較

(你可以跳過這一節先看第三節,重點在第三節)

從上一篇《【算法入門】廣度/寬度優先搜索(BFS) 》中知道,我們搜索一個圖是按照樹的層次來搜索的。

我們假設一個節點衍生出來的相鄰節點平均的個數是N個,那麼當起點開始搜索的時候,隊列有一個節點,當起點拿出來後,把它相鄰的節點放進去,那麼隊列就有N個節點,當下一層的搜索中再加入元素到隊列的時候,節點數達到了N2,你可以想想,一旦N是一個比較大的數的時候,這個樹的層次又比較深,那這個隊列就得需要很大的內存空間了。

於是廣度優先搜索的缺點出來了:在樹的層次較深&子節點數較多的情況下,消耗內存十分嚴重。廣度優先搜索適用於節點的子節點數量不多,並且樹的層次不會太深的情況。

那麼深度優先就可以克服這個缺點,因爲每次搜的過程,每一層只需維護一個節點。但回過頭想想,廣度優先能夠找到最短路徑,那深度優先能否找到呢?深度優先的方法是一條路走到黑,那顯然無法知道這條路是不是最短的,所以你還得繼續走別的路去判斷是否是最短路?

於是深度優先搜索的缺點也出來了:難以尋找最優解,僅僅只能尋找有解。其優點就是內存消耗小,克服了剛剛說的廣度優先搜索的缺點。

3.深度優先搜索


3.1.舉例

給出如圖3-1所示的圖,求圖中的V0出發,是否存在一條路徑長度爲4的搜索路徑。


3-1

顯然,我們知道是有這樣一個解的:V0->V3->V5->V6。

3.2.處理過程

3.3.對應例子的僞代碼

這裏先給出上邊處理過程的對應僞代碼。

  1. /** 
  2.  * DFS核心僞代碼 
  3.  * 前置條件是visit數組全部設置成false 
  4.  * @param n 當前開始搜索的節點 
  5.  * @param d 當前到達的深度,也即是路徑長度 
  6.  * @return 是否有解 
  7.  */  
  8. bool DFS(Node n, int d){  
  9.     if (d == 4){//路徑長度爲返回true,表示此次搜索有解  
  10.         return true;  
  11.     }  
  12.   
  13.     for (Node nextNode in n){//遍歷跟節點n相鄰的節點nextNode,  
  14.         if (!visit[nextNode]){//未訪問過的節點才能繼續搜索  
  15.   
  16.             //例如搜索到V1了,那麼V1要設置成已訪問  
  17.             visit[nextNode] = true;  
  18.   
  19.             //接下來要從V1開始繼續訪問了,路徑長度當然要加  
  20.   
  21.             if (DFS(nextNode, d+1)){//如果搜索出有解  
  22.                 //例如到了V6,找到解了,你必須一層一層遞歸的告訴上層已經找到解  
  23.                 return true;  
  24.             }  
  25.   
  26.             //重新設置成未訪問,因爲它有可能出現在下一次搜索的別的路徑中  
  27.             visit[nextNode] = false;  
  28.   
  29.         }  
  30.         //到這裏,發現本次搜索還沒找到解,那就要從當前節點的下一個節點開始搜索。  
  31.     }  
  32.     return false;//本次搜索無解  
  33. }  
/**
 * DFS核心僞代碼
 * 前置條件是visit數組全部設置成false
 * @param n 當前開始搜索的節點
 * @param d 當前到達的深度,也即是路徑長度
 * @return 是否有解
 */
bool DFS(Node n, int d){
    if (d == 4){//路徑長度爲返回true,表示此次搜索有解
        return true;
    }

    for (Node nextNode in n){//遍歷跟節點n相鄰的節點nextNode,
        if (!visit[nextNode]){//未訪問過的節點才能繼續搜索

            //例如搜索到V1了,那麼V1要設置成已訪問
            visit[nextNode] = true;

            //接下來要從V1開始繼續訪問了,路徑長度當然要加

            if (DFS(nextNode, d+1)){//如果搜索出有解
                //例如到了V6,找到解了,你必須一層一層遞歸的告訴上層已經找到解
                return true;
            }

            //重新設置成未訪問,因爲它有可能出現在下一次搜索的別的路徑中
            visit[nextNode] = false;

        }
        //到這裏,發現本次搜索還沒找到解,那就要從當前節點的下一個節點開始搜索。
    }
    return false;//本次搜索無解
}


3.4.DFS函數的調用堆棧


此後堆棧調用返回到V0那一層,因爲V1那一層也找不到跟V1的相鄰未訪問節點


此後堆棧調用返回到V3那一層


此後堆棧調用返回到主函數調用DFS(V0,0)的地方,因爲已經找到解,無需再從別的節點去搜別的路徑了。

4.核心代碼

這裏先給出DFS的核心代碼。

  1. /** 
  2.  * DFS核心僞代碼 
  3.  * 前置條件是visit數組全部設置成false 
  4.  * @param n 當前開始搜索的節點 
  5.  * @param d 當前到達的深度 
  6.  * @return 是否有解 
  7.  */  
  8. bool DFS(Node n, int d){  
  9.     if (isEnd(n, d)){//一旦搜索深度到達一個結束狀態,就返回true  
  10.         return true;  
  11.     }  
  12.   
  13.     for (Node nextNode in n){//遍歷n相鄰的節點nextNode  
  14.         if (!visit[nextNode]){//  
  15.             visit[nextNode] = true;//在下一步搜索中,nextNode不能再次出現  
  16.             if (DFS(nextNode, d+1)){//如果搜索出有解  
  17.                 //做些其他事情,例如記錄結果深度等  
  18.                 return true;  
  19.             }  
  20.   
  21.             //重新設置成false,因爲它有可能出現在下一次搜索的別的路徑中  
  22.             visit[nextNode] = false;  
  23.         }  
  24.     }  
  25.     return false;//本次搜索無解  
  26. }  
/**
 * DFS核心僞代碼
 * 前置條件是visit數組全部設置成false
 * @param n 當前開始搜索的節點
 * @param d 當前到達的深度
 * @return 是否有解
 */
bool DFS(Node n, int d){
    if (isEnd(n, d)){//一旦搜索深度到達一個結束狀態,就返回true
        return true;
    }

    for (Node nextNode in n){//遍歷n相鄰的節點nextNode
        if (!visit[nextNode]){//
            visit[nextNode] = true;//在下一步搜索中,nextNode不能再次出現
            if (DFS(nextNode, d+1)){//如果搜索出有解
                //做些其他事情,例如記錄結果深度等
                return true;
            }

            //重新設置成false,因爲它有可能出現在下一次搜索的別的路徑中
            visit[nextNode] = false;
        }
    }
    return false;//本次搜索無解
}


當然了,這裏的visit數組不一定是必須的,在一會我給出的24點例子中,我們可以看到這點,這裏visit的存在只是爲了保證記錄節點不被重新訪問,也可以有其他方式來表達的,這裏只給出核心思想。

深度優先搜索的算法需要你對遞歸有一定的認識,重要的思想就是:抽象!

可以從DFS函數裏邊看到,DFS裏邊永遠只處理當前狀態節點n,而不去關注它的下一個狀態。

它通過把DFS方法抽象,整個邏輯就變得十分的清晰,這就是遞歸之美。

5.另一個例子:24


5.1.題目描述

想必大家都玩過一個遊戲,叫做“24點”:給出4個整數,要求用加減乘除4個運算使其運算結果變成244個數字要不重複的用到計算中。

例如給出4個數:1234。我可以用以下運算得到結果24

1*2*3*4 = 242*3*4/1 = 24(1+2+3)*4=24;……

如上,是有很多種組合方式使得他們變成24的,當然也有無法得到結果的4個數,例如:1111

現在我給你這樣4個數,你能告訴我它們能夠通過一定的運算組合之後變成24嗎?這裏我給出約束:數字之間的除法中不得出現小數,例如原本我們可以1/4=0.25,但是這裏的約束指定了這樣操作是不合法的。

5.2.解法:搜索樹

這裏爲了方便敘述,我假設現在只有3個數,只允許加法減法運算。我繪製瞭如圖5-1的搜索樹。


5-1

此處只有3個數並且只有加減法,所以第二層的節點最多就6個,如果是給你4個數並且有加減乘除,那麼第二層的節點就會比較多了,當延伸到第三層的時候節點數就比較多了,使用BFS的缺點就暴露了,需要很大的空間去維護那個隊列。而你看這個搜索樹,其實第一層是3個數,到了第二層就變成2個數了,也就是遞歸深度其實不會超過3層,所以採用DFS來做會更合理,平均效率要比BFS快(我沒寫代碼驗證過,讀者自行驗證)。

6.OJ題目

題目分類來自網絡:

sicily1019 1024 1034 1050 1052 1153 1171 1187

pku1088 1176 1321 1416 1564 1753 2492 3083 3411

7.總結

DFS適合此類題目:給定初始狀態跟目標狀態,要求判斷從初始狀態到目標狀態是否有解。

8.擴展

不知道你注意到沒,在深度/廣度搜索的過程中,其實相鄰節點的加入如果是有一定策略的話,對算法的效率是有很大影響的,你可以做一下簡單馬周遊馬周遊這兩個題,你就有所體會,你會發現你在搜索的過程中,用一定策略去訪問相鄰節點會提升很大的效率。

這些運用到的貪心的思想,你可以再看看啓發式搜索的算法,例如A*算法等。

=========================================================

本文爲原創,轉載請註明出處:raphealguo@CSDN

作者:raphealguo(at)qq.com

時間:2012/05/12

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章