回溯法编程技巧

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种结果,深深地感受到递归的神奇。。。

posted @ 2019-01-28 16:36  残雪流年xpz  阅读(749)  评论(0编辑  收藏  举报