回溯法思想:
当把问题分成若干步骤并递归求解时,如果当前步骤没有合法选择,则函 数将返回上一级递归调用,这种现象称为回溯。正是因为这个原因,递归枚举算法常被称为 回溯法,应用十分普遍。
题目描述:
在棋盘上放置8个皇后,使得它们互不攻击,此时每个皇后的攻击范围为同行同列和同 对角线,要求找出所有解。
分析:
最简单的思路是把问题转化为“从64个格子中选一个子集”,使得“子集中恰好有8个格 子,且任意两个选出的格子都不在同一行、同
一列或同一个对角线上”。这正是子集枚举问 题。然而,64个格子的子集有264个,太大了,这并不是一个很好的模型。
第二个思路是把问题转化为“从64个格子中选8个格子”,这是组合生成问题。根据组合 数学,有 种方案,比第一种方案优秀,但仍然不够好。
经过思考,不难发现以下事实:恰好每行每列各放置一个皇后。如果用record[x]表示第x行 皇后的列编号,则问题变成了全排列生成问题。而0~7的排列一共只有8!=40320个,枚举量 不会超过它。
注意:在解决递归问题时,我们有时需要深入分析问题,对递归模型精雕细琢。一般还应 对解答树的结点数有一个粗略的估计,作为评价模型的重要依据,如图7-5所示。
图7-5中给出了四皇后问题的完整解答树。它只有17个结点,比4!=24小。为什么会这样 呢?这是因为有些结点无法继续扩展。例
如,在(0,2,*,*)中,第2行无论将皇后放到哪里,都 会和第0行和第1行中已放好的皇后发生冲突,其他还未放置的皇后更是如此。
在这种情况下,递归函数将不再递归调用它自身,而是返回上一层调用,这种现象称为 回溯(backtracking)。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1000;
bool vis[2][maxn];
int record[maxn][maxn];
int tot, n;
void search(int cur) {
if(cur==n) tot++;//tot答案个数
else {
for(int i = 0; i < n; i++) {
//不在一列以及一个对角线处
if(!vis[0][i] && !vis[1][cur+i] && !vis[2][cur-i+n]) {
//标记
vis[0][i] = vis[1][cur+i] = vis[2][cur-i+n] = true;
record[tot][cur] = i;//第tot记录在record中
search(cur+1);
//回溯要注意清除标记
vis[0][i] = vis[1][cur+i] = vis[2][cur-i+n] = false;
}
}
}
}
int main() {
// freopen("i.txt","r",stdin);
// freopen("o.txt","w",stdout);
tot = 0;
cin >> n;//确定棋盘大小
memset(vis, false, sizeof(vis));
memset(record, 0, sizeof(record));
search(0);
cout << tot << endl;
for(int i = 0; i < tot; i++) {
for(int j = 0; j < n; j++) {
if(record[j]) //打印每种情况的n个皇后
cout << j << " " << record[i][j] << endl;
}
cout << "-------------------------------------" << endl;
}
return 0;
}
上面的程序有个极其关键的地方:vis数组的使用。vis数组的确切含义是什么?它表示 已经放置的皇后占据了哪些列、主对角线
和副对角线。将来放置的皇后不应该修改这些值——至少“看上去没有修改”。一般地,如果在回溯法中修改了辅助的全局变量,则一定要及 时把它们恢复原状(除非故意保留所做修改)。若不信,可以把“vis[0][i]= vis[1][cur+i] = vis[2][cur-i+n] = 0”注释掉,
验证还能否正确求解八皇后问题。另外,在调用之前一定要把 vis数组清空。
如果在回溯法中使用了辅助的全局变量,则一定要及时把它们恢复原状。 特别地,若函数有多个出口,则需在每个出口处恢复被修改的值。
抄自紫书