來自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。