用STL實現DFS/BFS演算法——檢查重複狀態

來源:互聯網
上載者:User
用STL實現DFS/BFS演算法 ——檢查重複狀態前幾天,有網友留言說我使用“先深”、“先寬”的說法是狗屁二流子詞彙,正確的說法應該是“深度優先”,“廣度優先”。態度和用詞雖然有點讓人不好接受,但提的意見終歸是有點道理的。請實話,我已經不記得最早是從哪裡學來的“先深”、“先寬”,但可以肯定一點,本人是發明不出這樣的詞彙的,只是認為這兩個詞還不錯,比較簡單明了,所以就用上了。後來,我上GOOGLE查了一下,結果發現的確用四個字的比用兩個字多得多,看來多數人還是喜歡長一點的詞,或者說長一點的那兩個詞更通用一些。於是我就想是不是該“改正”一下自己的“錯誤”,換個大家更容易接受的詞用用。接下來,很高興地看到有網友挺“先深”、“先寬”,於是我又想,如果我這就換了,豈不是有點對不起那些和我一樣喜歡用“先深”、“先寬”的網友?思前想後,乾脆用E文好了,所以就有了這個新的標題。轉入正題,前面我們說過,到目前為止我們的DFS/BFS演算法還只能搜尋無重複狀態的狀態空間樹,而象下象棋、推箱子等問題,都會出現問題狀態在幾步之後可能會回到以前出現過的狀態的情況。當重複狀態出現時,我們必須能夠識別出來,並且不應把它放入搜尋樹中。最簡單、直接的做法就是,當問題狀態類的nextStep()成員函數返回一組下一步可能狀態時,我們對這些狀態逐一進行檢查,如果發現有重複的,就不把重複狀態放入搜尋樹(即不入棧或入隊列)。至於如何檢查重複狀態,我們還是採用函數模板最常見的方法,讓使用者自己提供檢查的方法,在我們的DFS/BFS演算法中用一個泛型的模板參數來指定它。這樣一來,我們的演算法會變為這樣(以BFS為例):template <class T1, class T2, class T3 >int BreadthFirstSearch(const T1& initState, const T2& afterFindSolution,    T3& checkDup)// 前兩個參數與舊版本相同// checkDup : 仿函式,對於每一個下一步可能的狀態調用之,它接受一個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 { // 不是目標狀態,判斷是否放入搜尋隊列中              if (!checkDup(*i)) // 只將不重複的狀態放入搜尋隊列              {                    states.push(*i);              }          }      } } return n;} 與舊版本的相比,我們可以看到,它只是增加了一個模板參數T3、一個調用參數checkDup和一行代碼if (!checkDup(*i))。所有增加的東西都很清楚明白,就不用多說了。現在,使用BFS的使用者要多提供一個函數對象(或仿函式)類型,該函數對象負責儲存傳入的狀態物件(類型為T1)並進行檢查,看看傳入的狀態物件是否與以前傳入的某個對象重複。為了減輕BFS使用者的負擔,我認為應該提供幾個簡單的檢查方法供使用者選用,只有我們提供的方法都不適用時,使用者才需要自行提供。第一個最簡單的檢查方法就是不檢查,即與我們的舊版本BFS一樣。不檢查的方法很簡單,根據這個BFS演算法的要求,我們直接返回false就行了。如下:template <class T>struct NoCheckDup : std::unary_function<T, bool>{    bool operator() (const T&) const    {        return false;    }}; 在這裡,我們可以再多做一些事情,讓我們的新版本BFS與舊版本相容,即提供一個與舊版本相同的介面,這樣可以讓使用者更方便使用。辦法也很簡單,定義一個重載的BreadthSearchFirst,它接受兩個參數(和舊版本一樣),然後使用一個NoCheckDup<T1>對象作為第三參數調用新版本演算法就行了。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 : 找到的答案數量{    NoCheckDup<T1> noCheckDup;    return BreadthFirstSearch(initState, afterFindSolution, noCheckDup);} 有了這個重載版本,我們前面的數獨問題程式和N皇后問題程式都可以無須修改,重編譯一下就可以了。接下來,我們還可以再實現幾個簡單的重複狀態檢查方法,當然了,它們都會使用STL的容器和演算法來實現。用STL來進行查重,可以有很多方法,我想到了最常用的三種:線性複雜度的find演算法、對數複雜度的set容器,和速度更快但也是最複雜的hash容器。其中最後一種hash容器還不是STL的標準實現,不過多數STL實現都提供了,我們不妨試著用一下。下面,我們一個一個來看看如何?。首先是線性尋找方法,我們可以用一個vector容器來儲存所有傳入的狀態,然後用find演算法來進行線性尋找,代碼如下;// 仿函式,用vector容器檢查狀態結點是否重複,線性複雜度// 要求狀態類提供operator==template <class T>class SequenceCheckDup : std::unary_function<T, bool>{    typedef vector<T> Cont;    Cont states_;public:    bool operator() (const T& s)     {        typename Cont::iterator i =             find(states_.begin(), states_.end(), s);        if (i != states_.end()) // 狀態已存在,重複        {            return true;        }        states_.push_back(s); // 狀態未重複,記錄該狀態        return false;    }}; 為什麼選用vector呢?我們的需求很簡單:可以把狀態物件依次放入容器、可以執行find演算法就行了。當然,用deque和list都是可以的,而且時間複雜都是相同層級的(因為插入元素的次序並不重要,我們可以簡單地選擇後端插入)。這樣,我就選擇了佔用空間最少的vector了。為了使find演算法可以執行,對狀態類T提出了一個要求,它必須提供operator==。這一點由狀態類的提供者(即BFS的使用者)負責。當BFS的使用者給出一個支援operator==的問題狀態類MyState後,他就可以這樣使用新版本的BFS:    MyState initState(n);    SequenceCheckDup<MyState> checkDup;    int total = BreadthFirstSearch(initState, continueSearch, checkDup); 還有一點要注意的是,與NoCheckDup不同,SequenceCheckDup是有狀態的,使用同一個對象兩次調用SequenceCheckDup的結果是不同的,對SequenceCheckDup的調用有可能會改變函數對象的狀態。所以,它的operator()不可以是const的。使用這種有狀態的函數對象要格外小心,這是題外話,就不多說了。你可以想象,線性尋找的速度是不能令人滿意的。當搜尋樹增大到一定程度後,線性尋找的速度會越來越慢。BFS在每次產生一個新的狀態結點時,都會調用一次checkDup,所以如果搜尋樹的結點數為n,則BFS的時間複雜度為O(n*n)。我們知道STL中的set容器正是為提高容器內元素搜尋的速度而設計的,如果使用set來替代vector,可以期望BFS的時間複雜度降為O(n*logn)。這樣,我們有了第二個查重方法OrderCheckDup,如下:// 仿函式,用set容器檢查狀態結點是否重複// 要求狀態類提供operator<template <class T>class OrderCheckDup : std::unary_function<T, bool>{    typedef set<T> Cont;    Cont states_;public:    bool operator() (const T& s)     {        typename Cont::iterator i = states_.find(s);        if (i != states_.end()) // 狀態已存在,重複        {            return true;        }        states_.insert(i, s); // 狀態未重複,記錄該狀態        return false;    }}; 與SequenceCheckDup相比,OrderCheckDup把存放已有狀態物件的容器從vector換成了set,把原來的通用find演算法換成了set::find(),把原來的vector::push_back換成了set::insert(),僅此而已。與SequenceCheckDup不同的是,由於OrderCheckDup要把狀態物件存入set,所以它要求狀態類T提供operator<,而不是operator==。所以,如果你想使用OrderCheckDup,你就要給你的問題狀態類MyState提供operator<;至於BFS的使用方法,則與前相同:MyState initState(n);OrderCheckDup<MyState> checkDup;int total = BreadthFirstSearch(initState, continueSearch, checkDup); 通常來說,OrderCheckDup提供的O(n*logn)時間複雜度已經很不錯了,對於絕大多數問題來說都是合適的。但有的時候,使用者可能要追求更高的查重速度,這時可以考慮使用hash容器。這就是我們的第三個查重方法HashCheckDup,如下:// 仿函式,用hash_set容器檢查狀態結點是否重複// 要求狀態類提供operator==以及hash函數template <class T, class HashFcn = hash<T> >class HashCheckDup : std::unary_function<T, bool>{    typedef hash_set<T, HashFcn> Cont;    Cont states_;public:    typedef typename Cont::hasher hasher;    HashCheckDup(const hasher& hf) : states_(100, hf) {}    bool operator() (const T& s)     {        if (states_.find(s) != states_.end()) // 狀態已存在,重複        {            return true;        }        states_.insert(s); // 狀態未重複,記錄該狀態        return false;    }}; 它使用了尚未正式進入C++標準的hash_set,不過有很多STL實現都提供這個容器,具體的說明可以去看相關文檔,這裡只說一點,hash_set容器要求存入的物件類型T要提供operator==和一個hash函數。所以我們可以看到,HashCheckDup除了把set換成了hash_set,它還多了一個模板參數和一個建構函式(即hash函數)。Hash函數實現的好壞直接關係到hash_set容器進行尋找的速度,沒有一種通用的hash演算法,所以必須由類型MyState的提供者來提供適用於MyState的hash演算法,它的格式應該是這樣的:struct HashMyState{    size_t operator() (const MyState& s) const    {      …    }}; 當你為MyState準備好了operator==以及hash演算法後,可以這樣來調用BFS:MyState initState(n);// 以下變數定義必須通過添加括弧來消除二義性,否則編譯器會把它看作函式宣告HashCheckDup<MyState, HashMyState> checkDup((HashMyState()));int total = BreadthFirstSearch(initState, continueSearch, checkDup); 這段代碼中有一個很有趣的現象,即在checkDup的定義中,用於調用建構函式的實參多了一重括弧。一眼看上去,這重括弧好象是多餘的,但其實不是。如果沒有這重括弧,這段代碼是通不過編譯的,編譯器會抱怨說在下一行代碼中的BreadthFirstSearch()模板函數執行個體化有錯,不能把MyState轉換為HashMyState (*)()。開始的時候,我也被弄糊塗了,只依稀記得在哪見過這個錯誤。一番尋找後,在《Exception C++ Style》的第29條找到了答案。原來在沒有多一重括弧時,編譯器把這行代碼:HashCheckDup<MyState, HashMyState> checkDup(HashMyState());看作是一個函式宣告而不是變數定義!書中給出了最簡單的解決方案:通過添加一對括弧來消除二義性。這就是這對看似多餘的括弧之所以存在的原因。現在我們有了總共四個尋找重複狀態的方法:不尋找、線性尋找、二分尋找(set容器提供的其實就是一種有序容器上的二分尋找)和hash尋找。使用者可以根據需要選用它們中的一個。不同的方法對於問題狀態類有不同的要求,總結如下:
尋找方法 對問題狀態類的要求
不尋找 僅適用於不會出現重複狀態的情況,對問題狀態類無特別要求
線性尋找 要求問題狀態類提供operator==,尋找速度慢
二分尋找 要求問題狀態類提供operator<,尋找速度較快
Hash尋找 要求問題狀態類提供operator==以及hash演算法,如果hash演算法合適的話,尋找速度最快
如果這四種方法都不合你意,你可以自行提供尋找演算法並用它來調用BFS好了。說了這麼多,我想也應該用一個實際的問題例子來檢驗一下我們的新版BFS了,我挑選的例子是幾年前做過一道題目:推箱子;與數獨問題和N皇后問題相比,推箱子遊戲要複雜不少,所以下一篇文章可能會多花一點時間。 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.