用STL實現先深搜尋及先寬搜尋演算法
來源:互聯網
上載者:User
用STL實現先深搜尋及先寬搜尋 先深搜尋和先寬搜尋演算法是對問題狀態空間樹(state space tree)進行搜尋的兩種方法。問題狀態空間樹是指用樹的結構來表示所有的問題狀態,樹中的每一個結點確定了所求解問題的一個問題狀態(problem state);樹中有些結點代表的是答案狀態(answer state),即是那些滿足求解條件的狀態。要從一棵問題狀態空間樹中找出滿足求解條件的答案狀態結點,辦法有兩種:先深搜尋法DFS(depth first search)和先寬搜尋法BFS(breadth first search)。兩種搜尋方法都可以確保找到問題的答案(如果有的話),其主要差異在於採用不同的搜尋策略,從而對於不同的問題,兩種搜尋方法的效率可能有所不同。另外,如果要求找出最低代價(如最少的步數)的解法,則通常只能使用BFS。不過這些都不是這篇文章所關心的,大家如果有興趣,可以找找相關的演算法書籍。這裡要討論的是,如何使用Template和STL來實現一個較為通用(當然也還不是萬能的)DFS和BFS。首先,我們要關注的是問題狀態空間樹,通常這棵樹非常之大,結點非常之多,建立一棵完整的狀態空間樹通常需要耗費大量的時間,甚至會遠遠超出從這棵樹中搜尋出答案的時間。試想一下,如果我們可以用程式建立出這麼一棵完整的狀態空間樹,那麼我們只要在建立的時候稍微多做一點事情,即在產生每個結點時檢查一下,不就可以輕而易舉地找出所有答案了嗎?所以,先將整棵狀態空間樹建立起來,再進行搜尋的方法是行不通的。更為合理的方法是:邊建立狀態空間樹,邊進行搜尋。我的想法是:用一個容器來表示狀態空間樹,用演算法逐步產生樹的下一層結點,每產生一個結點就放入容器中;同時,每次從容器中取出一個結點檢查其是否滿足條件,如果是則表示找到一個答案狀態結點,否則就以該結點為輸入產生下一步的狀態結點。開始時,容器中只有一個結點,既代表初始問題的狀態結點,以此為搜尋演算法的起點進行迭代來找答案,根據問題的不同要求,我們可以在找到一個答案後停止搜尋,也可以繼續搜尋以找出所有答案。如果用容器來表示狀態空間樹,DFS和BFS對容器有什麼不同的要求呢?我們來看一示: 假如我們有初始問題狀態A,從A可以產生下一層(或者說下一步)的狀態B、C、D,同樣從B可以產生下一層的狀態E、F、G。對於DFS,我們的搜尋順序應該是A、B、E…,結合我們前面所說的邊產生樹邊搜尋的方法,這些狀態的產生次序應該是A、B、C、D、E、F、G…,所以用棧(stack)來作為存放狀態樹的容器就最合適了。我們把初始狀態A入棧,然後按以下方法進行迭代:從棧中取出一個結點(狀態),根據該狀態產生下一層結點(所有可能的下一步狀態),逐一壓入棧中(同一層的結點入棧的順序不太要緊,為了易於表述,我們假設以D、C、B的順序入棧);如此類推,取出B,產生E、F、G入棧。對於狀態是否滿足解答條件的判斷可以在入棧前或出棧後進行,當然出於效率的考慮,我們建議在入棧前進行判斷,這樣合格答案狀態就可以不必入棧了。我們再來看看BFS,正確的搜尋順序應該是A、B、C、D、E…,這個順序與狀態的產生順序完全相同,所以這次應該用隊列(queue)來作為存放狀態樹的容器了。我們把初始狀態A放入隊列,然後按以下方法進行迭代:從隊列中取出一個結點(狀態),根據該狀態產生下一層結點(所有可能的下一步狀態),逐一放入隊列中;如此類推,取出B,產生E、F、G放入隊列。同樣,我們在把狀態放入隊列前進行是否滿足解答條件的判斷,這樣合格答案狀態就可以不必放入隊列了。通過以上的分析,我們已經有了演算法的大概輪廓了。但我們還缺少三樣東西,一個是如何從一個狀態產生下一步狀態,另一個是如何判斷是否得到了答案狀態,還有一個就是得到答案狀態後如何辦。顯然,這些東西都是與具體問題相關的,最好的辦法就是把它們作為模板參數,這樣的話我們的演算法就可以有最廣泛的適用範圍了。那麼,我們是否需要三個模板參數呢?個人認為,前兩樣東西屬於如何構造一個具體問題的解法,而最後一樣東西則是指找到答案後的處理方法。所以,我把前兩者封裝成一個類(對應於狀態空間樹中的結點,該結點知道如何產生下一層結點,也知道自己是否滿足解答條件),而把後者實現成一個函數對象。#include <stack>#include <vector> using std::stack;using std::vector; template <class T1, class T2>int DepthFirstSearch(const T1& initState, const T2& afterFindSolution)// initState : 初始化狀態,類T1應提供成員函數nextStep()和isTarget(),// nextStep(vector<T1>&)用於返回下一步可能的所有狀態,// isTarget()用於判斷目前狀態是否符合要求的答案;// afterFindSolution : 仿函式,在找到一個有效答案後調用之,它接受一個const T1&,// 並返回一個Boolean值,true表示停止搜尋,false表示繼續找// return : 找到的答案數量{ int n = 0; stack<T1> states; states.push(initState); vector<T1> nextStates; bool stop = false; while (!stop && !states.empty()) { T1 s = states.top(); states.pop(); nextStates.clear(); s.nextStep(nextStates); for (typename vector<T1>::iterator i = nextStates.begin(); i != nextStates.end(); ++i) { if (i->isTarget()) { // 找到一個目標狀態 ++n; if (afterFindSolution(*i)) // 處理結果並決定是否停止搜尋 { stop = true; break; } } else { // 不是目標狀態,放入搜尋隊列中 states.push(*i); } } } return n;}程式比較簡單,相信大家能夠看懂。關鍵有以下幾點:1. 類T1由演算法使用者提供,它必須具有可以表示問題狀態的資料成員,並提供兩個成員函數:void nextStep(vector<T1>&)和boolean isTarget();前者以自身狀態為起點,返回所有可能的下一步狀態,由於可能的下一步狀態數量不定,所以需要用vector<T1>&來返回;後一個成員函數則比較簡單,返回一個boolean值來判斷自身狀態是否滿足解答條件。2. 類T2為函數指標類型或函數物件類型,該函數接受一個const T1&參數,並返回一個boolean值,傳入的參數即為搜尋演算法找到的一個答案狀態,函數可以按自己的方法處理它(如列印到終端,或寫入到檔案等),然後返回一個boolean值來表示是否繼續搜尋其它答案(有一些問題只要求找到一個答案即可,而另一些問題則要求找出所有答案)。3. stack<T1> states就是用於存入狀態空間樹的棧容器,就象我們前面所分析的那樣,使用棧容器可以很好地類比出先深搜尋DFS。 看過了DFS,相信大家都知道BFS也會和DFS差不了多少。以下是My Code:#include <queue>#include <vector> using std::queue;using std::vector; template <class T1, class T2>int BreadthFirstSearch(const T1& initState, const T2& afterFindSolution)// initState : 初始化狀態,類T1應提供成員函數nextStep()和isTarget(),// nextStep()用vector<T1>返回下一步可能的所有狀態,// isTarget()用於判斷目前狀態是否符合要求的答案;// afterFindSolution : 仿函式,在找到一個有效答案後調用之,它接受一個const T1&,// 並返回一個Boolean值,true表示停止搜尋,false表示繼續找// return : 找到的答案數量{ int n = 0; queue<T1> states; states.push(initState); vector<T1> nextStates; bool stop = false; while (!stop && !states.empty()) { T1 s = states.front(); states.pop(); nextStates.clear(); s.nextStep(nextStates); for (typename vector<T1>::iterator i = nextStates.begin(); i != nextStates.end(); ++i) { if (i->isTarget()) { // 找到一個目標狀態 ++n; if (afterFindSolution(*i)) // 處理結果並決定是否停止搜尋 { stop = true; break; } } else { // 不是目標狀態,放入搜尋隊列中 states.push(*i); } } } return n;}它和DFS幾乎一模一樣,除了把stack換成了queue,把top()換成了front(),所以我想也不用再作解釋了吧。 為了檢查演算法的有效性,我選了前段時間很熱門的一個遊戲——數獨sudoku,實作出一個簡單的解法,測試了一下這兩個DFS和BFS演算法。這個話題就留到下一次再寫吧。