八皇后问题是一个关于回溯算法的典型例题,所谓回溯,就是按选优条件向前搜素,以达到目标。但当搜索到某一步时,发现原先选择并不是最优或者达不到目标,则退一步重新选择。具体问题描述如下:在8X8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线(对角线)上,问有多少种摆法。
下面先以四皇后问题来说明回溯法的基本思想:先在4X4的方格的第一行第一列的位置上放置一个皇后,如图1所示,则灰色阴影部分都是它的攻击范围,现在我们从第二行开始放置第二个皇后,先放置在第二行第一列,显然此位置在第一个皇后的攻击范围之内,所以将第二个皇后的位置右挪一位,它仍处于第一个皇后的攻击范围之内,继续右移,直到第二个皇后的位置不在第一个皇后的攻击范围之内,此时由两个皇后构成的攻击范围如图2所示。接下来开始再第三行放置皇后,但是此时整个第三行都在前两个皇后的攻击范围内,导致这种放置方法不能继续下去,这时候我们退回一步,重新放置第二个皇后的位置,如图3所示。这时候,再从第三行第一列的位置开始放置皇后,直到前三行的皇后都互不攻击,就开始在下一行放置皇后,如果有冲突,就返回一步,重新放置上一行的皇后位置,如果上一行的皇后位置已经是最右边,则再退回一行,将上上一行的皇后位置向右挪动一位,但最后一行的皇后摆放以后,没有互相攻击,说明所有皇后已经摆放好,此时将计数器加1。然后将最后一行的皇后位置再右挪一位,寻找下一种互不攻击的摆放方法,经过这样的一个过程,就可以寻找出所有的摆放方法。
该算法的重点步骤如下:
1, 从第一行开始放置皇后,当第i行放好以后,在放置第i+1行时,从左边第一列开始放置,如果有冲突,皇后往右挪一位,同时判断是否越界,写一个函数用来判断当前位置是否可以放置皇后;
2,如果最后一行位置的皇后也已经放置好了,说明一种成功摆放方法已经出现,计数器+1,;
3,当出现一种摆放方法以后,还要试探其它方法,可以把最后最后一行的皇后“拿走”,然后再试探其它未试探过的棋盘格;
4,如果该位置不能放置皇后,要把它置0,如果该位置可以放置皇后就置1,当一种摆放方法成功以后,将皇后“拿走”的操作也是置0;
下面讨论具体的程序、
定义一个NxN的二维数组来表示棋盘,chess[i][j]表示第i行第j列处的元素,变量nCount用来统计最终的摆放次数,棋盘的行和列的表示如图4,
#include<iostream>
using namespace std;
#define N 8 //可以根据N来修改棋盘的格数
int nCount = 0; //设置一个计数器
int chess[N][N] = { 0 }; //用于存放棋盘的二维数组
用来判断棋盘当前位置chess[i][j]是否可以放置皇后的函数
//判断在当前位置放入皇后以后是否会和以前的皇后相互攻击
//返回1 表示攻击范围互相重叠
//返回0 表示攻击范围不会重叠,可以在此处放置皇后
int check(int i, int j)//i = 7,j = 4
{
int k = 0;
for (k = 0; k <= i; k++) //判断纵向是否有皇后
{
if (chess[k][j] == 1)
return 1;
if (j + k < N&&chess[i - k][j + k] == 1) // 判断右侧对角线是否有皇后
return 1;
if (j - k >= 0 && chess[i - k][j - k] == 1) //判断左侧对角线是否有皇后
return 1;
}
return 0;
}
递归的主要算法
- void queen(int i, int j)
- {
- if (j == N) //判断列数是否越界,如果当前行的八个位置都不能放置皇后,
- return; //当前的递归结束,返回到上一层
- if (check(i, j) == 0) //判断当前位置是否能放皇后
- {
- chess[i][j] = 1; //如果可以放置,则置1
- if (i == N - 1) //第七行已经放置好,已经成功找到一种摆放方法,将计数器加1,并将棋盘打印
- {
- nCount++;
- print(); //打印
- }
- else //没有到第七行,则从第i+1行第一列的位置继续摆放
- queen(i + 1, 0);
- }
- chess[i][j] = 0;
- queen(i,j + 1); //第j列不能放置皇后,向右挪一位
- }
queen函数是解决八皇后问题的核心,程序以i=0,j=0的参数进入。在程序18行中,将chess[i][j]置0的原因有两个:
1,在将第i行的皇后摆放好以后,在放置第i+1行的皇后时,发现第i+1行已经不能再放置皇后,这时递归需要退回一步,将上一行已经放置好的皇后“拿走”,即置0,然后向右移动一位,继续判断下一个位置的合法性,
2,当第七行的皇后已经摆放好,说明已经找到一种成功的方法,此时将计时器+1,然后将最后一行的皇后“拿走”,即置0,然后继续右移,继续寻找下一种摆放方法。
打印函数
void print()//打印函数
{
int i = 0;
int j = 0;
cout<<"*****************************************"<<endl;
for (i = 0; i < N; i++)
{
for (j = 0; j < N; j++)
cout << chess[i][j] << " ";
cout << endl;
}
cout << "*****************************************" << endl;
}
完整代码如下:
#include <iostream>
using namespace std;
#define N 8 //可以根据N来修改棋盘的格数
int nCount = 0; //设置一个计数器
int chess[N][N] = { 0 }; //用于存放棋盘的二维数组
void print()//打印函数
{
int i = 0;
int j = 0;
cout<<"*****************************************"<<endl;
for (i = 0; i < N; i++)
{
for (j = 0; j < N; j++)
cout << chess[i][j] << " ";
cout << endl;
}
cout << "*****************************************" << endl;
}
//判断在当前位置放入皇后以后是否会和以前的皇后相互攻击
//返回1 表示攻击范围互相重叠
//返回0 表示攻击范围不会重叠,可以在此处放置皇后
int check(int i, int j)
{
int k = 0;
for (k = 0; k <= i; k++) //判断纵向是否有皇后
{
if (chess[k][j] == 1)
return 1;
if (j + k < N&&chess[i - k][j + k] == 1) // 判断右侧对角线是否有皇后
return 1;
if (j - k >= 0 && chess[i - k][j - k] == 1) //判断左侧对角线是否有皇后
return 1;
}
return 0;
}
//递归的主要算法
void queen(int i, int j)
{
if (j == N) //判断列数是否越界,如果当前行的八个位置都不能放置皇后,
return; //当前的递归结束,返回到上一层
if (check(i, j) == 0) //判断当前位置是否能放皇后
{
chess[i][j] = 1; //如果可以放置,则置1
if (i == N - 1) //第七行已经放置好,已经成功找到一种摆放方法,将计数器加1,并将棋盘打印
{
nCount++;
print(); //打印
}
else //没有到七行,则从第i+1行第一列的位置继续摆放
queen(i + 1, 0);
}
chess[i][j] = 0;
queen(i,j + 1); //第j列不能放置皇后,向右挪一位
}
int main(void)
{
queen(0, 0);
cout << "nCount=" << nCount << endl;
return 0;
}
运行结果如下
程序运行结果为92,说明八皇后问题一共有92种摆放方法,这与八皇后问题的实际解是一样的。
在上面的讨论中,我们是利用一个二维数组chess[N][N]来表示一个NxN的棋盘的,chess[i][j]表示第i行第j列的皇后状态。事实上,我们要在每一行都放置一个皇后,那么八皇后问题的解可以表示为{(1,X1)、(2,X2)、(3,X3)、(4,X4)、(5,X5)、(6,X6)、(7,X7)、(8,X8)},其中(1,X1)表示第一个皇后的位置在第一行第X1列的位置上(Xi的取值范围是1到8),由于行号是固定的,因此可以简记为(X1,X2,X3,X4,X5,X6,X7,X8)。所以,我们可以使用一个一维数组chess[N]来表示八皇后的状态,chess[i] 中下标i表示皇后的行数,值表示皇后所处的列数,(i的取值范围是1到8,chess[i]的取值也是1到8),比如chess[3]=5就表示,第三个皇后处于第三行第五列的位置上,下面我们分析当用一个一维数组时怎么解决皇后之间的冲突问题,如下图所示,
(1),两个皇后不在同一列: chess[i] != chess[j],表示位于第i行和第j行的皇后不在同一列;
(2),两个皇后不在同一条主对角线上:如上面左图,同一颜色的区域代表皇后在同一条对角线上,同一颜色的皇 后位置满足 chess[i] - chess[j] = i-j;要想皇后位置不冲突,须满足 chess[i] - chess[j] != i-j;
(3),两个皇后不在同一条次对角线上:如上面右图,同一颜色的皇后位置满足 chess[i] - chess[j] = j-i;
综合上面三种情况,当用一维数组记录皇后位置时,皇后位置不冲突的判断条件为:
abs( chess[i] - chess[j] ) != abs(i-j) 或者 chess[i] != chess[j]。检查函数如下:
//判断在当前位置放入皇后以后是否会和以前的皇后相互攻击
//返回0 表示攻击范围互相重叠
//返回1 表示攻击范围不会重叠,可以在此处放置皇后
bool Check(int n)
{
for (int i = 0; i < n;i++)
{
if (abs(chess[n] - chess[i]) == abs(n - i) || (chess[n] == chess[i]))
//绝对值括号里面的一项用来排除两条对角线冲突的情况,后面一项排除同一列有冲突的情况
return false;
}
return true;
}
接下来,开始逐行放置皇后,先从第1行开始,也就是在chess[0]放置皇后,chess[0]里面存储的是皇后在第一行的列数,其取值范围为0到7,接下来开始在第2行放置皇后,也就是在chess[1]的位置上放置0到7,如果有冲突,换数字即可,如果放到7以后还有冲突,退出当前的递归层,返回上一层,将上一行的皇后位置右挪一位(即chess[0]里的数字加1);如果不冲突,进入下一层,直到第8行(即chess[7])放置以后,还没有发生冲突,则找到一种组合。递归函数如下:
void Queen(int n)
{
for (int i = 0; i < N;i++)
{
chess[n] = i; //从第n行的第0列开始放置皇后
if (Check(n)) //检查放在第n行第i的皇后是否有冲突
{
//若没有冲突
if (n==N-1) //第七行新放置的皇后不冲突,说明八个皇后全部放置完毕
{
nCount++; //计数器加1
Show(); //打印放置成功的棋盘
}
else
Queen(n + 1); //若还没有到最后一层,递归调用,摆放下一层
}
}
}
打印函数如下:
void Show()
{
for (int i = 0; i < N;i++)
{
cout << chess[i] << " ";
}
cout << endl;
}
完整代码如下:
#include <iostream>
#include<math.h>
using namespace std;
#define N 8
int chess[N] = { 0 };
//用一个一维的数组来存储皇后的位置,
// chess[i] 中下标i表示皇后的行数,值表示皇后所处的列数
//i的取值范围是0到7,chess[i]的取值也是0到7
int nCount;
//判断在当前位置放入皇后以后是否会和以前的皇后相互攻击
//返回0 表示攻击范围互相重叠
//返回1 表示攻击范围不会重叠,可以在此处放置皇后
bool Check(int n)
{
for (int i = 0; i < n;i++)
{
if (abs(chess[n] - chess[i]) == abs(n - i) || (chess[n] == chess[i]))
//绝对值括号里面的一项用来排除两条对角线冲突的情况,后面一项排除同一列有冲突的情况
return false;
}
return true;
}
void Show()
{
for (int i = 0; i < N;i++)
{
cout << chess[i] << " ";
}
cout << endl;
}
void Queen(int n)
{
for (int i = 0; i < N;i++)
{
chess[n] = i; //从第n行的第0列开始放置皇后
if (Check(n)) //检查放在第n行第i的皇后是否有冲突
{
//若没有冲突
if (n==N-1) //第七行新放置的皇后不冲突,说明八个皇后全部放置完毕
{
nCount++; //计数器加1
Show(); //打印放置成功的棋盘
}
else
Queen(n + 1); //若还没有到最后一层,递归调用,摆放下一层
}
}
}
int main()
{
Queen(0); //先从第0行开始放置
cout << nCount << endl;
return 0;
}
运行结果如上图,输出的每一行表示chess数组的各个元素,分别代表第一行到第八行的皇后所在列数,程序最后输出八皇后问题的所有组合解。通过改变宏定义里的N,可以得到任意皇后数目的解。