1. 程式人生 > >回溯法編程技巧

回溯法編程技巧

過程 指數 並且 selected 有一個 文字 pri -h 之前

1. 什麽是回溯法

引用一下維基百科對回溯法的介紹:

回溯法(英語:backtracking)是暴力搜索法中的一種。

對於某些計算問題而言,回溯法是一種可以找出所有(或一部分)解的一般性算法,尤其適用於約束滿足問題(在解決約束滿足問題時,我們逐步構造更多的候選解,並且在確定某一部分候選解不可能補全成正確解之後放棄繼續搜索這個部分候選解本身及其可以拓展出的子候選解,轉而測試其他的部分候選解)。

在經典的教科書中,八皇後問題展示了回溯法的用例。(八皇後問題是在標準國際象棋棋盤中尋找八個皇後的所有分布,使得沒有一個皇後能攻擊到另外一個。)

回溯法采用試錯的思想,它嘗試分步的去解決一個問題。在分步解決問題的過程中,當它通過嘗試發現現有的分步答案不能得到有效的正確的解答的時候,它將取消上一步甚至是上幾步的計算,再通過其它的可能的分步解答再次嘗試尋找問題的答案。回溯法通常用最簡單的遞歸方法來實現,在反復重復上述的步驟後可能出現兩種情況:

  1. 找到一個可能存在的正確的答案
  2. 在嘗試了所有可能的分步方法後宣告該問題沒有答案

在最壞的情況下,回溯法會導致一次復雜度為指數時間的計算。

2. 回溯法編程解決n皇後問題

之前並未試過利用回溯法編程解決問題,在看了《關於八皇後問題以及回溯遞歸思想》這篇博文後收獲頗豐,在此感謝作者。我在其基礎上給出了利用回溯法,使用C++編程解決n皇後問題的代碼如下

 1 #include <iostream>
 2 #include <vector>
 3 using namespace std;
 4 
 5 void print(vector<vector<int
> > &arry, int &sum, int n) {//打印結果 6 cout << "方案" << sum << ":" << "\n"; 7 for (int i = 0; i < n; ++i) { 8 for (int j = 0; j < n; ++j) { 9 if (arry[i][j] == 1) { 10 cout << "O "; 11 } 12 else
{ 13 cout << "+ "; 14 } 15 } 16 cout << endl; 17 } 18 cout << endl; 19 } 20 21 bool check(vector<vector<int> > &arry, int row, int column, int n) {//判斷節點是否合適 22 for (int i = 0; i < row; ++i) {//檢查列沖突 23 if (arry[i][column] == 1) { 24 return false; 25 } 26 } 27 for (int i = row - 1, j = column - 1; i >= 0 && j >= 0; --i, --j) {//檢查左對角線 28 if (arry[i][j] == 1) { 29 return false; 30 } 31 } 32 for (int i = row - 1, j = column + 1; i >= 0 && j <= n - 1; --i, ++j) {//檢查右對角線 33 if (arry[i][j] == 1) { 34 return false; 35 } 36 } 37 return true; 38 } 39 40 void findQueen(vector<vector<int> > &arry, int &sum, int row, int n) {//尋找皇後節點 41 if (row == n) {//n皇後的解 42 ++sum; 43 print(arry, sum, n);//打印n皇後的解 44 return; 45 } 46 47 for (int column = 0; column < n; ++column) {//深度回溯,遞歸算法 48 if (check(arry, row, column, n)) {//檢查皇後擺放是否合適 49 arry[row][column] = 1; 50 findQueen(arry, sum, row + 1, n); 51 arry[row][column] = 0;//清零,以免回溯的時候出現臟數據 52 } 53 } 54 } 55 56 int main() { 57 int n; 58 cout << "n皇後問題求解,請輸入自然數n(輸入ctrl+z退出程序):" << endl; 59 while (cin >> n) { 60 vector<vector<int> > arry(n, vector<int>(n, 0));//棋盤,放皇後 61 int sum = 0;//存儲方案結果數量 62 cout << n << "皇後問題的解為:" << endl; 63 findQueen(arry, sum, 0, n); 64 cout << n << "皇後問題共有:" << sum << "種可能" << endl; 65 cout << "----------------------------------------" << endl; 66 cout << "n皇後問題求解,請輸入n(輸入ctrl+z退出程序):" << endl; 67 } 68 return 0; 69 }

3. 回溯法編程技巧小結

通過以上n皇後問題回溯法的求解過程,個人認為類似的回溯法求解問題可以借鑒以上的幾點編程技巧:

  1. 核心函數(如上文中的findQueen)是一個遞歸函數,首先應當編寫遞歸結束的條件,也就是找到了問題的解的情況對應的代碼(如增加解的個數,輸出當前找到的解等)。其次應當遍歷當前步驟可能的解,若當前步驟的解滿足則添加當前解並進行遞歸。最後一定要記得遞歸退出意味著下一輪沒有解符合要求,於是當前解也不符合要求,因此需要消除前面添加當前解的步驟的影響,如上文的“清零,以免回溯的時候出現臟數據”這個步驟(回溯思想的體現)。
  2. 每一步的解的檢查需要一個輔助函數,這個可以根據題意進行編寫,如上文中的check函數。

通過以上編程技巧的小結,我發現以前遇到的一道編程題可以通過這些技巧解決。(當時還未學過回溯法,不會寫。)

4. 回溯法的應用

題目:有一個自然數集合,其中最小的數是1,最大的數是100。這個集合中的數除了1之外,每個數都可由集合中的某兩個數相加而得(這兩個數可以相同)。利用回溯法編寫程序,求符合上述條件的、元素個數為10的所有集合。

分析:由題意可知,這10個元素中必然包含1和100,所有的元素除了1都是另外兩個數相加得到或是另一個數的兩倍。我們不妨限定最後得到的10個數按升序排列(x1,x2,x3,x4,x5,x6,x7,x8,x9,x10)。由於較大的數是由兩個較小的數相加或一個較小的數的兩倍,因此較大數的範圍是[(1+max(集合)),(2*max(集合)),例如若已找到(x1,x2,x3),則x4的範圍是[x3+1,2 * x3]。因此算法思路是輸入一個擁有一個元素的集合,不斷獲取下一個較大的數,並判斷是否滿足題目條件,直到集合中共有9個元素為止,最後將100加入集合並判斷是否滿足題目條件,滿足則結束遞歸並輸出結果,否則回溯到上一步並將上一個元素加1再次進行判斷,直到當前步驟所有元素均測試完畢。

文字分析可能沒有講得很清楚,看代碼有助於理解,C++代碼如下:

 1 #include <iostream>
 2 #include <vector>
 3 using namespace std;
 4 
 5 void print(vector<int> &vecs) {
 6     for (auto x : vecs) {
 7         cout << x << " ";
 8     }
 9     cout << 100 << endl;
10 }
11 
12 bool check(vector<int> &vecs, int num) {
13     for (int x : vecs) {
14         if (x > 99) return false;
15     }
16     for (int x : vecs) {
17         for (int y : vecs) {
18             if (x + y == num) return true;
19         }
20     }
21     return false;
22 }
23 
24 void findNext(vector<int> &vecs, int &sum) {
25     if (vecs.size() == 9) {
26         if (check(vecs, 100)) {
27             print(vecs);
28             ++sum;
29         }
30         return;
31     }
32     //vecs中最後一個元素即為最大元素
33     int min = 1 + (*vecs.rbegin());
34     int max = 2 * (*vecs.rbegin());
35     for (int i = min; i <= max; ++i) {
36         if (check(vecs, i)) {
37             vecs.push_back(i);
38             findNext(vecs, sum);
39             vecs.pop_back();
40         }
41     }
42 }
43 
44 int main() {
45     int sum = 0;
46     vector<int> vecs;
47     vecs.push_back(1);
48     findNext(vecs, sum);
49     cout << "一共有" << sum << "種結果。" << endl;
50     return 0;
51 }

由於我這道題是動態添加和減少集合中的元素,於是遞歸結束的條件可以利用集合中元素的個數,不需要另外的變量來記錄遞歸層數。最後一共得到2215種結果,深深地感受到遞歸的神奇。。。

回溯法編程技巧