DFS和BFS遍歷的問題

來自https://github.com/soulmachine/leetcodenode

廣度優先搜索c++

輸入數據:沒有什麼特徵,不像dfs須要有遞歸的性質。若是是樹/圖,機率更大。git

狀態轉換圖:數或者DAG圖(有向無環圖)github

求解目標:求最短算法

思考的步驟:編程

1,是求路徑長度,仍是路徑自己(動做序列)數組

  a,若是是求路徑長度,則狀態裏面要存路徑長度(或雙端隊列+一個全局變量)緩存

  b,若是是求路徑自己或動做序列數據結構

    i,要用一顆樹存儲寬搜過程的路徑函數

    ii,是否可以預算狀態個數的上限?

      可以預估狀態總數,則開闢一個大數組,用樹的雙親表示法;

      若是不能預估狀態總數,則要使用一顆通用的樹,這也是第4步的須要不充分條件。

2,如何表示狀態?即一個狀態須要存儲哪些必要的數據,纔可以完整提供如何擴展到下一步狀態的全部信息。通常記錄當前位置或總體局面。

3,如何擴展狀態?這一步和第2步有關,狀態裏記錄的數據不一樣,擴展方法就不一樣。

  對於固定不變的數據結構(通常題目直接將給出,做爲輸入數據),如二叉樹、圖。擴展方法簡單,直接往下一層走。

  對於隱式圖,要先在第一步裏想清楚狀態所帶的數據,想清楚了這一點,就能夠直到如何擴展了。

4,如何判斷重複?

  若是狀態轉換圖是一棵樹,則永遠不會出現迴路,不須要判重。

  若是狀態轉換圖是一個圖(這時候是一個圖上的BFS),則須要判斷重複。

    a,若是是求最短路徑長度或一條路徑,則只須要讓「點」(就是狀態)不重複出現,便可保證不出現迴路

    b,若是是求全部路徑,注意此刻,狀態轉換圖是DAG,即容許兩個父節點指向同一個字節點。具體實現時,每一個節點要「延遲」加入到已訪問集合visited,

     要等一層所有訪問完後,再加入到visited集合。

    c,具體實現?

      i,狀態是否存在完美哈希方案?即將狀態一一映射到整數,互相之間不會衝突。

      ii,若是不存在,則須要使用通用的哈希表(本身實現,或使用STL,例如unordered_set)來判重;

        本身實現的哈希表,若是可以預估狀態個數的上限,則能夠開兩個數組,head和next表示哈希表(下面有例子)。

      iii,若是存在,則能夠開一個大布爾數組,來判重,且此刻能夠精確計算出狀態總數,而不只僅是預估上限。

5,目標狀態是否已知?

  若是題目已經給出了目標狀態,能夠帶來很大便利,這時候能夠從起始狀態出發,正向廣搜,

  也能夠從目標狀態出發,逆向廣搜,

  也能夠同時出發,雙向廣搜。

-----

代碼模板

廣搜須要一個隊列,用於一層一層擴展,一個hashset,用於判重,一棵樹(只求長度時不須要)用於存儲整棵樹。

  對於隊列,能夠用queue,也能夠把vector看成隊列使用。當求長度時,有兩種作法:

    1,只用一個隊列,但在狀態結構體state_t裏面增長一個整數字段level,表示當前所在的層次,當碰到目標狀態時,直接輸出level便可。

    這個方案能夠很容易編程A*搜索,把queue替換爲priority_queue便可。

    2,用兩個隊列,current,next,分別表示當前層次和下一層,另設一個全局整數level,表示層數(即路徑長度),當碰到目標狀態,輸出level便可。

    這個方案,狀態裏能夠村路徑長度,只需全局設置一個整數level,比較節省內存;

  對於hashset

    若是有完美哈希方案,用布爾數組(bool visited[STATE_MAX]或vector<bool> visited(STATE_MAX,false)來表示;

    若是沒有完美哈希方案,須要用STL裏的set或unordered_set。

  對於樹,

    若是用STL,能夠用unordered_map<state_t,state_t> father表示一棵樹,代碼很簡潔。

    若是可以預估狀態總數的上限(設爲STATE_MAX),可使用數組state_t nodes[STATE_MAX],即樹的雙親表示法來表示樹,效率更高,可是代碼更高。

代碼在這裏

 

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

深度優先搜索

適用場景:

輸入數據,若是是遞歸數據結構,如單鏈表,二叉樹,集合,則百分百能夠用深搜;若是是非遞歸數據結構,如一維數組,二維數組,字符串,圖則機率小一些。可是也有的。

狀態轉換圖,樹或者圖

求解目標:必須走到最深處(例如對於樹,必需要走到葉子節點)才能獲得一個解,適合是恩搜。

 

思考步驟:

1,深搜常見的三個問題,求可行解的總數,求一個可行解,求全部可行解。

  a,若是是路徑條數,則不須要存儲路徑

  b,若是哦是路徑自己,則要用一個數組path存儲路徑。

    跟寬搜不一樣,寬搜雖然也是一條路徑,可是須要存儲擴展過程當中的全部路徑,在沒找到答案以前全部路徑都不能放棄。

    而深搜,在搜索過程當中始終只有一條路徑,所以用一個數組就能夠了。

2,只要求一個解,仍是求全部解。

  只求一個解,找到一個解就返回。

  求全部解,找到一個後,還要繼續遍歷。

  廣搜通常只要求一個解,(廣搜也會求全部解,這時須要擴展到全部葉子節點,至關於在內存中存儲整個狀態轉換圖,很是佔內存,所以廣搜不適合求這類問題)。

3,若是表示狀態?

  即一個狀態須要存儲哪些必要的數據,纔可以完整提供如何擴展到下一步狀態的全部信息。跟廣搜不一樣,深搜的慣用寫法,不是把數據記錄在狀態struct裏,而是

  添加函數參數(有時爲了節省遞歸堆棧,用全局變量),struct裏的字段與函數參數一一對應。

4,如何擴展狀態?

  這一步跟上一步相關。狀態裏記錄的數據不一樣,擴展方法就不一樣,對於固定不變的數據結構(通常題目直接給出,做爲輸入數據),二叉樹、圖,擴展方法很簡單,直接往下一步走就好了。對於隱式圖,要先在第1步中想清楚狀態所帶的數據,才能直到擴展。

5,終止條件?

  是指到了不能擴展的末端節點。對於樹,是葉子節點。對於圖或隱式圖,是出度爲0的節點。

6,收斂條件?

  是指找到一個合法解的時刻。

  若是正向深搜(父節點處理完了,才進行遞歸,即父狀態不依賴子狀態,遞歸語句在最後,尾遞歸),則是指是否達到目標狀態;

  若是是逆向搜索,(處理父狀態時須要先知道子狀態的結果,此時遞歸語句不在最後),則是指是否到達初始狀態。

 

  不少時候,終止狀態和收斂條件是合二爲一的,不少人不會區分這兩種條件。仔細區分這兩種條件,是有必要的。

 

  爲了判斷是否到了收斂條件,要在函數接口裏用一個參數記錄當前的位置(或距離目標還有多遠)。

  若是是求一個解,直接返回這個解;若是是求全部解,要在這裏蒐集,即把第一步中表示路徑的數組path[]複製到解集合裏。

7,關於判重

  a,是否須要判重?

    若是狀態轉換圖是一顆樹,則不須要判重,由於在遍歷的過程當中不會出現重複;

    若是狀態狀態圖是一個DAG,則須要判重。這一點和BFS不同,BFS的狀態轉換圖老是DAG,必須判重。

  b,怎麼判重,跟廣搜同樣。同時,DAG說明存在重疊子問題,此時能夠用緩存加速。見第8步(下一步)。

8,如何加速?

  a,剪枝。深搜必定要好好考慮怎麼剪枝,成本小收益大,加幾行代碼,就能大大加速。

      這裏沒有通用的方法,只能具體問題,具體分析,要充分觀察,充分利用各類信息來剪枝,在中間節點提早返回。

  b,緩存。

    i,前提條件:狀態轉換圖是一個DAG。DAG=>存在重疊子問題=>子問題的解會被重複利用,用緩存天然會由加速效果。

     若是依賴關係是樹狀的(例如樹,單鏈表等),不必加緩存,由於子問題只會一層層往下,用一次就不再會用到,加了緩存也沒什麼效果。

    ii,具體實現:可使用數組或hashmap。

      維度簡單的,用數組;

      維度複雜的,用hashmap,c++有map,c++11之後有unordered_map,比map快。

代碼模板

/**
*@brief dfs模板
*@param[in] input 輸入數據指針
*@param[out] path 當前路徑,也是中間結果
*@param[out] result 存放最終結果
*@param[inout] cur or gap 標記當前位置或距離目標的距離
*@return  1,路徑長度,2路徑自己
*/
void dfs(type &input,type &path,type &result,int cur or gap){
    if(data is valid) return;//終止條件
    if(curr==input.size()){// 收斂條件,正向
        ///if(gap==0){}///逆向
        result.push_back(path);
    }
    
    if(能夠剪枝) return;
    
    for(...){///執行全部的可能的擴展
        執行動做,修改path
        dfs(input,step+1,or gap--,result);
        恢復path
    }
}        

---------

深搜和回溯法?

深搜:維基百科

回溯法:維基百科

回溯法=深搜+剪枝,通常在用深搜時,或多或少會用到剪枝,所以深搜和回溯法沒有什麼不同的。

 

深搜與遞歸recursion?

深搜,是邏輯意義上的算法;遞歸是物理意義上的實現,遞歸和迭代iteration相對應。

深搜,能夠用遞歸實現,也能夠用棧實現。而遞歸,通常老是用來實現深搜,能夠說,遞歸必定是深搜,可是深搜不必定遞歸。

遞歸由兩種加速策略,1剪枝,對中間結果判斷,提早返回。2緩存,緩存中間結果,防止重複計算,用空間換時間。

其實,遞歸+緩存,就是memorization(翻譯爲備忘錄法),就是top-down with cache(自頂向下+緩存),它是Donald Michie在1968年創造的術語,表示

  一種優化技術,在top-down形式的程序中,使用來避免重複計算,能夠加速。

memorization不必定用遞歸,就像深搜不必定用遞歸同樣,能夠在迭代iterative中使用memorization。遞歸也不必定用memorization,可使用它來加速,但也不是必須的。

  只有在使用了緩存時,它纔是memorizaiton。

相關文章
相關標籤/搜索