回溯法是一种能解决百分之九十九的问题的一种通用的算法思想,在问题的解空间树中,按深度优先策略对整个解空间树进行搜索,当算法搜索到某一个节点如果不包含问题的解则会直接跳过对这个子树的搜索回溯到这个节点的根节点,继续深度优先搜索。回溯法几个重要的名词:活结点,拓展节点,死节点,约束函数,限界函数。
活结点,拓展节点,死节点:从一个节点出发搜索的时候,这个节点就是活结点,同时也会成为拓展节点,在拓展节点搜索到新的节点时就会成为新的活结点并且进行拓展,当拓展节点不能进行纵深方向移动的时候就会成为死节点。
回溯法有两种方法:递归回溯和迭代回溯
递归和迭代回溯的主要区别就是是否用递归的方法来进行条件判断后的下一步的搜索。递归利用的是递归函数,而迭代则要再添加一个while循环。
回溯法的算法框架有两种分别是:子集树和排列树
两种框架主要区别就是叶子结点的个数不同,也就代表了搜索的时间复杂度不同,遍历子集树的任何算法需要(2^n)的时间,而遍历排列树需要
(n!)的时间。
今天解决的N后问题是用的递归回溯的方法。
问题介绍:在n*n的棋盘格上放置n个皇后,保证n个皇后之间不在同一行、不在同一列且不在同一斜线。求满足条件的解的个数和皇后的位置。
例:8皇后问题的一个解
算法思想:这个问题的关键就是用数组x[i]=j来表示列,以及是否在斜线方向出现冲突的的判定方法,斜线的判定方法可以用两点间的斜率为1来判断,除法不好计算而可以转换为乘法(X-i)*(X-i) == (Y-i)*(Y-i)。在满足判断函数check()后,递归的调用search()来搜索答案,通过prt()输出答案,约束条件就是不同列和不同斜线。代码注释写的也很详细了。具体的代码如下:
#include <iostream>
using namespace std;
int x[20]; //表示第i行的皇后在第x[i]列
int n, ans; //n表示皇后个数,ans表示解的总数
//结果输出函数
void prt(int x[], int n){
cout <<"皇后所在的列数分别为:";
for(int i = 1; i <= n; i++){
cout <<x[i]<<" ";
}
cout <<endl;
return;
}
//检查(X,Y)放入皇后后,是否会与(i,x[i])产生冲突
//true:不会冲突,false:会冲突
bool check(int x[], int X, int Y){
int i,j;
bool conf = false; //不会冲突
for(i = 1; i <= X-1 && !conf; i++){
j = x[i];
if(j == Y || (X-i)*(X-i) == (Y-j)*(Y-j)){
conf = true;
}
}
return !conf;
}
void search (int m){
for(int j = 1; j <= n; j++){
if(check(x, m, j)){
x[m] = j;
search(m+1);
}
}
if(m == n+1){ //1..m行的皇后填满了
prt(x, n);
ans++;
}
}
int main(){
cout <<"请输入棋盘规格:";
cin >>n;
search(1);
if(ans == 0)
cout <<"No answer!";
return 0;
}
程序运行的测试图如下:
如果使用迭代回溯,可以省去一定的时间消耗,需要声明一个新的数组存放找到的解,在循环判断的时候对这个数组不必在此求是否满足条件。