问题抛出:在棋盘上放置8个皇后,使得它们互不攻击,此时每个皇后的攻击范围为同行同列和同对角线,要求找出所有解。如图所示:
【分析】
最简单的方法是把问题转化为“从64个格子中选一个子集”,使得“子集中恰好有8个格子,且任意两个选出的格子都不在同一行、同一列、或同一个对角线上”。这正是子集枚举问题。然而,64个格子的子集有2^64个,数量级有10^19,太大了,这并不是一个很好的模型。
简化:把问题转化为“从64个格子中选8个格子”,这是组合生成问题。根据组合数学,有8C64 = 4.426*10^9种方案,少很多了,很还不够少。
再简化:观察规律,恰好每行每列各放置一个皇后。如果用C[x]表示第x行皇后的列编号,则问题变成了全排列生成问题。而0~7的排列一共只有8!=40320个,枚举量不会超过它。
注意:当把问题分成若干步骤并递归求解时,如果当前步骤没有合法选择,则函数将返回上一级递归调用,这种现象成为回溯。正是因为这个原因,递归枚举算法常被成为回溯(backtracking)法,应用十分普遍。
demo 1
#include <iostream>
#include <cstdio>
using namespace std;
int n, tot;
int C[8];
void search(int cur)
{
if (cur == n) { // 递归边界。只要走到这里,所有皇后必然不冲突
tot++;
}
else for (int i = 0; i < n; i++) {
bool ok = true;
C[cur] = i; // 尝试把第cur行的皇后放在第i列
for (int j = 0; j < cur; j++) { // 检查是否和前面你的皇后冲突
if (C[cur] == C[j] ||
cur - C[cur] == j - C[j] || // 主对角线判定
cur + C[cur] == j + C[j]) { // 副对角线判定
ok = false;
break;
}
}
if (ok) {
search(cur + 1);
}
}
}
int main()
{
cin >> n;
tot = 0;
search(0);
cout << tot << endl;
return 0;
}
注意:既然是逐行放置的,则皇后肯定不会横向攻击,因此只需检查是否纵向和斜向攻击即可。
结点数现在很难减少了,但程序效率可以继续提高;利用二维数组vis[2][]直接判断当前尝试的皇后所在的列和两个对角线是否已有其他皇后。注意到主对角线标识y-x可能为负数,存取时要加上n。
demo 2
#include <iostream>
#include <cstdio>
using namespace std;
int n, tot;
int C[8];
int vis[3][15];
void search(int cur)
{
if (cur == n) {
tot++;
}
else for (int i = 0; i < n; i++) {
if (!vis[0][i] && !vis[1][cur + i] && !vis[2][cur - i + n]) {
C[cur] = i;
vis[0][i] = vis[1][cur + i] = vis[2][cur - i + n] = 1; // 修改全局变量
search(cur + 1);
vis[0][i] = vis[1][cur + i] = vis[2][cur - i + n] = 0; // 记得改回来
}
}
}
int main()
{
cin >> n;
tot = 0;
memset(vis, 0, sizeof(vis));
search(0);
cout << tot << endl;
return 0;
}
demo 2程序有个及其关键的地方:vis数组的使用,vis数组的确切含义:它表示已经放置的皇后占据了哪些列、主对角线和副对角线。将来放置的皇后不应该修改这些值。一般地,如果在回溯法中修改了辅助的全局变量,则一定要及时把它们恢复原状(除非故意保留所做修改)。另外,在调用之前一定要把vis数组清空。
注意:如果在回溯法中使用了辅助的全局变量,则一定要及时把它们恢复原状。特别地,若函数有多个出口,则需要在每个出口处恢复被修改的值。