问题引入 八皇后问题
八皇后问题,是一个古老而著名的问题,是回溯算法的典型案例。该问题是国际西洋棋棋手马克斯·贝瑟尔于1848年提出:在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。
问题中涉及了回溯算法,它就是一种深度优先搜索算法,每走一步,选择其中的一种可能解,满足就继续走下一步,不满足就回到上一步选择另一种可能的解
下图有助于理解皇后问题的回溯法解决(有其它的解法)
在该问题中了解了回溯算法的流程之后我们需要考虑什么时候回溯,也就是每次放置皇后需要考虑的问题:
放置皇后时是否存在同一行,同一列,互为为对角线位置的皇后存在?
假设该皇后放置在坐标(i,j)的位置,每次检查时检查前i-1行就不会涉及到同一行的情况,同一列问题只需要在放置前n-i行的时候标记皇后所在的列数就能解决,而对角线位置则需要前i-1个皇后的坐标与当前皇后的坐标斜率不为正负1
在很多情况下八皇后都被推广到n皇后、2n皇后等,并且有一些附加的条件需要考虑,但是归根到底都是对回溯算法的一种考查。
先看看非递归版的
这里我以我洛谷的一道题目 P1219八皇后 来展示N皇后的非递归解法
输入输出样例
输入
6
输出
2 4 6 1 3 5
3 6 2 5 1 4
4 1 5 2 6 3
4
下方是我的题解和注释
#include<iostream>
#include<cmath>
#include<cstring>
using namespace std;
int queen[14]; //判断行
int col[14];//判断列,一开始以queen[i]的值表示皇后位于第几列(递归版的就是这么做的)
//但是每次检查的时候都要遍历queen[],最后一个测试点超时了,改了一下,增加了一个数组进行表示
int n;
int total = 0;
bool HasSpace(int i, int j) //检查当前棋盘格子是否可以放置皇后
{
if (col[j])return false; //检查同一列
for (int k = 1; k < i; k++) //检查时只要检查到第i行之前
if (abs(k - i) == abs(queen[k] - j))return false; //对角线为正负1
return true;
}
void Print() //打印皇后的位置
{
for (int i = 1; i <= n; i++)
cout << queen[i] << " ";
cout << endl;
}
int main()
{
int i, j;
int temp = 0; //temp初始化为0
//memset(queen,0,sizeof(queen)); 因为queen[]和col[]是全局变量自动初始化为0
//memset(col, 0, sizeof(col)); 如果不是设置为全局的需要手动初始化
cin >> n;
for (i = 1; i <= n; i++)
{
int flag = 1;
for (j = temp + 1; j <= n; j++)
{
temp = 0; //用完temp重置为0
if (HasSpace(i, j)) {
queen[i] = j;
col[j] = 1; //表示该列有皇后
flag = 0; //表示可以放
if (i == n) { //满足n个皇后都放完
total++;
if (total <= 3)Print(); //根据题意打印前三种情况
/*完成一种情况,回溯*/
//每次回溯都要把上一行的标记去除,成功的情况下当前行的标记也要去掉
col[queen[i]] = 0; //去掉当前行标记
queen[i] = 0;
i -= 2; //去掉上一行标记
temp = queen[i + 1]; //记录回溯到上一个的位置
col[temp] = 0;
queen[i + 1] = 0;
}
break;
}
}
if (flag) { //没找到,回溯
if (i == 1 && j == n + 1) {
break; //回溯到第一行末表示结束
}
i -= 2; //去掉上一行标记,因为第一个for循环后面有i++的条件,所以这里i减少2
temp = queen[i + 1]; //记录回溯到上一个的位置,下一次循环j从temp+1开始
col[temp] = 0; //将该列置0
queen[i + 1] = 0;
}
}
cout << total << endl;
return 0;
}
递归版
递归版的算法一般比较代码整洁,效率差,理解起来也不太容易
下方代码采用C语言描述(因为大学课程安排问题,平常都是几种语言混用)
(注意:此代码并非针对上方的P1219八皇后题目的题解)
#include<stdio.h>
#include<math.h>
#define MAX 6
int queen[MAX]; //下标代表行,值代表列(注意下标从0开始)
int HasSpace(int n) //n,queen[n],n皇后是否能放置在queen[n]列上
{
int i;
for ( i = 0; i < n ; i++) //扫描前n-1行
{
/*queen[i]==queen[n]表示同一列有皇后 两个abs相等表示有位于同一对角线的皇后*/
if (queen[i] == queen[n] || abs(i - n) == abs(queen[i] - queen[n]))
{
return 0;
}
}
return 1;
}
void PrintfQueen()
{
int i;
for (i = 0; i < MAX; i++) //输出每一行皇后的位置
printf("%d", queen[i]+1); //数组的下标从0开始,输出结果要加1
printf("\n");
}
void PlaceQueen(int n) //表示放置第n个皇后
{
/*
看起来并没有非递归版的直观易理解,
因为它的回溯过程是通过递归时的压栈出栈来实现的
当一个皇后放置成功,进行PlaceQueen(n+1)放置下一个皇后
当该皇后放置失败,会跳出当前的PlaceQueen(n+1)回到PlaceQueen(n)然后j++继续搜索
可以模拟一下递归过程来理解
*/
int j;
for (j = 0; j < MAX; j++)
{
queen[n] = j; //先把皇后放下去,再判断是否放置正确
if (HasSpace(n)) { //如果放置正确,分两种情况
if (n == MAX - 1)PrintfQueen(); //放满则打印
else PlaceQueen(n + 1); //否则放置下一个皇后
}
}
}
int main()
{
PlaceQueen(0); //从第一行的皇后开始放
return 0;
}
最后
前文也有提过n皇后问题有其它的解法
例如通过位运算和数学构造法,奈何笔者才疏学浅,感兴趣的可以自己搜索