问题表述为:在8×8格的国际象棋上摆放8个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。
思路1
- 棋盘是8乘8块格子,那么用8乘8的数组来表示棋盘是合适的
- 一个位置能否放置格子,得看他的衡竖方向的直线,以及斜线是否有皇后,如果有,则不能放置。如图:
这里k表示斜率,以(0,0)为坐标原点的话,两条斜线的斜率分别为1和-1。那么根据点斜式:y - y0 = k(x - x0),变式为:y = kx +b。
所以这两条斜线的方程为:y = x + y0 -x0,以及y = x0 + y0 - x;另外两条更是十分轻松,分别是,y = y0,x = x0。所以,根据这些原理编写一个是否能放置皇后位置的函数,该函数接受一个坐标(x,y)以及表示棋盘的二维数组,若可以放置就返回1,否则返回0:
int check_queen(int x,int y,int queens[][8])
{
//标志量,都为1则表示可放置皇后,默认为可放置皇后
int flag_x = 1;
int flag_y = 1;
int flag_NE = 1;
int flag_NW = 1;
//x方向上的检查,按行放置的话,这段代码可免。建议先把下面这段注释取消掉
/**
for( int i = 0;i < MAX_SIZE; i++){
if( queens[x][i] == 'Q'){
flag_x = 0;
break;
}
}
*/
//y方向上的检查 这两个代码块其实是可以合并成一个代码块的,但是也会带来潜在的隐患
for( int i = 0; i< MAX_SIZE; i++){
if(queens[i][y]){
flag_y = 0;
break;
}
}
// 由斜率 y - y1 = k(x - x1 ) 变换而来的 y = kx + b
int b1 = y -x;
int b2 = x + y;
//斜率为1方向上的检查
for( int i = 0; i < MAX_SIZE; i++){
//之所以用cotinue是因为有些斜线所穿过的格子并不全在8乘8的棋盘里
if( i + y - x > 7 || i + y - x < 0)continue;
if( queens[i][ i + y - x]){
flag_NE = 0;
break;
}
}
//斜率为-1方向上的检查,这两个虽然也可以进行合并,但是由于判断条件不同,所以,不推荐进行合并
for( int i = 0; i < MAX_SIZE; i++){
if( x + y - i > 7 || x + y -i < 0)continue;
if( queens[i][ x + y - i]){
flag_NW = 0;
break;
}
}
if( flag_x && flag_y && flag_NE && flag_NW){
return 1;
}
else{
return 0;
}
}
3.接下来便是问题的关键。
我们怎么放置皇后呢?
默认的思路是。从第0行开始,检查当前位置是否能放置一个皇后,若不能则寻找该行的下一个能放置皇后的位置,若找到了一行的列尾部,那么程序就该返回。若能放置皇后,就放置皇后,并进入下一行,然后进入第一步,直到找到最后一行的皇后被放置,然后返回。根据这个思路,我们编写函数如下(该函数接受一个行数,和一个表棋盘的二维数组):
void travelsing_chessBoard(int cols,int queens[][8])
{
for( int i = 0; i < MAX_SIZE; i++)
{
if( check_queen(cols,i,queens) ){
queens[cols][i] = 'Q';
//成功放置一个皇后,则进入下一行的皇后检查,如果已经是最后一行,那么就返回
if( cols == MAX_SIZE - 1)
{
//打印棋盘
print_chessBoard(queens);
return;
}
travelsing_chessBoard( cols + 1,queens);
}
}
//如果出了行的遍历,就代表到达了行末还没有找到可以放皇后的位置应该返回
return;
}
在这里先放出程序的主体:
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#define MAX_SIZE 8
int Chess_manual = 0;
int check_queen(int x,int y,int queens[][8]);
/*
*参数列表的cols和rows是x坐标和y坐标,表面目前检查的位置
*/
//棋盘打印函数
void print_chessBoard(int queens[][8]);
void travelsing_chessBoard(int cols,int queens[][8])
int main()
{
int queens[MAX_SIZE][MAX_SIZE] = {0};
travelsing_chessBoard(0,queens);
printf("\n共有%d张棋谱",Chess_manual);
}
当把检查函数(检查函数x的方向上注释应该去掉)和遍历函数填进去,执行后,程序输出0个棋盘。
这是为什么呢?很明显,我们人工遍历一次就可以知道了。
这就是刚才程序的执行结果
第五行的皇后被放置之后,第六行便没有合适的位置来放置皇后呢。而此时皇后的放置行数没有达到最后一行,所以不会打印棋盘。
//成功放置一个皇后,则进入下一行的皇后检查,如果已经是最后一行,那么打印棋盘,然后返回
if( cols == MAX_SIZE - 1)
{
print_chessBoard(queens);
return;
}
所以程序最后到第6行的末尾,然后返回,即:
//如果出了行的遍历,就代表到达了行末还没有找到可以放皇后的位置应该返回
return;
就这样,程序返回到了第五行所放置皇后的下一个位置,因为该位置有皇后了,所以后面的位置都不能放皇后,所以这样一直返回到了第0行,程序结束。
这就暴露了刚才所编写函数的缺点了。
- 死路问题:当k(k > 1)行不能放置皇后,程序返回到(k - 1)行后,该行皇后位置并没有做出更改。导致程序不能走其他路,就陷入了死路。所以,我们应该移动k-1行的皇后的位置,然后重新走一遍。也就是说:当k行无法放皇后的时候,我们应该移动k-1行皇后的位置,然后在走一遍。
所以对函数改进一下(注释掉x方向上的检查),变为:
int rows_note = 1; //用于记录本行皇后的位置
int num = 0; // 用于记录本行皇后的数量
for( int i = 0; i < MAX_SIZE; i++)
{
if( check_queen(cols,i,queens) ){
//当一行放置了大于一个的皇后的时候,移除掉前面放置的皇后。因为是按行的顺序放置的,所以说,行的检查是要避免的
if(num > 0){
queens[cols][rows_note] = 0;
num--;
}
queens[cols][i] = 'Q';
rows_note = i;
num++;
//成功放置一个皇后,则进入下一行的皇后检查,如果已经是最后一行,那么就返回
if( cols == MAX_SIZE - 1)
{
//打印棋盘
print_chessBoard(queens);
return;
}
travelsing_chessBoard( cols + 1,queens);
}
}
//如果出了行的遍历,就代表到达了行末还没有找到可以放皇后的位置应该返回
return;
执行后,函数依然是0张棋谱,这是为什么?我们在跟着程序返回一遍。
因为即使我们想更改第五行的皇后的位置,但是并没有第五行第二个位置可以放皇后了,所以程序返回到第四行放皇后的地方。同样是没有合适的位置,就这样一直放回到第0行。这一切的根源是我们想更改第五行的皇后的位置时,没有合适的位置了,但是我们没有清空他,导致第四行皇后取舍的时候会受到它的英雄。这也是一个死路问题。没有位置再放第二个,就代表该行的遍历出了for循环,,所以我们在该函数的最后一个return前加上这句:
queens[cols][rows_note] = 0;
再次运行或发现,程序的输出结果只有一次(字符Q的值是81)。即:多次运行都是这个结果,这是为什么呢?
很明显,当每一行都正确放置了皇后后,函数就直接返回了。因为什么?其实皇后放到最后一行,也是死路,虽然是我们要的结果,但我们并没有清除,也就是说函数返回时,他依然会发生我们前面说的低层皇后影响上一层皇后的取舍问题。所以,当最后一行的皇后成功放置时,打印棋盘后,将皇后去掉。即:
queens[cols][rows_note] = 0;
在调用打印棋盘函数之后加上这句,程序便可正确输出了。
可见,主要是死路问题阻挠了程序的执行。
但死路的表现形式又是多种多样化的,有行内的影响,有上下行的影响。
另外每一条代码并不都是全时段有效代码,比如,当成功找到最后一行的一个皇后,程序还试图在该行寻找第二个皇后等。是无效也是无意义的。但是,如果删掉这一步骤,则有多个合适位置放皇后的行便会收到影响。也就是说,是否进行优化,还是有讨论性的。
这就是本文的完整代码
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#define MAX_SIZE 8
int Chess_manual = 0;
int check_queen(int x,int y,int queens[][8])
{
int flag_x = 1;
int flag_y = 1;
int flag_NE = 1;
int flag_NW = 1;
//x方向上的检查,按行放置的话,这段代码可免
/**
for( int i = 0;i < MAX_SIZE; i++){
if( queens[x][i] == 'Q'){
flag_x = 0;
break;
}
}
*/
//y方向上的检查 这两个代码块其实是可以合并成一个代码块的,但是也会带来潜在的隐患
for( int i = 0; i< MAX_SIZE; i++){
if(queens[i][y]){
flag_y = 0;
break;
}
}
// 由斜率 y - y1 = k(x - x1 ) 变换而来的 y = kx + b
int b1 = y -x;
int b2 = x + y;
//斜率为1方向上的检查
for( int i = 0; i < MAX_SIZE; i++){
//之所以用cotinue是因为有些斜线所穿过的格子并不在8乘8的棋盘里
if( i + y - x > 7 || i + y - x < 0)continue;
if( queens[i][ i + y - x]){
flag_NE = 0;
break;
}
}
//斜率为-1方向上的检查,这两个虽然也可以进行合并,但是由于判断条件不同,所以,不推荐进行合并
for( int i = 0; i < MAX_SIZE; i++){
if( x + y - i > 7 || x + y -i < 0)continue;
if( queens[i][ x + y - i]){
flag_NW = 0;
break;
}
}
if( flag_x && flag_y && flag_NE && flag_NW){
return 1;
}
else{
return 0;
}
}
/*
*参数列表的cols和rows是x坐标和y坐标,表面目前处理的位置
*/
void print_chessBoard(int queens[][8])
{
printf("\n**************************************************************\n");
for(int i = 0; i < MAX_SIZE; i++){
for(int j = 0; j < MAX_SIZE; j++){
printf(" %d ",queens[i][j]);
}
printf("\n");
}
Chess_manual++;
}
void travelsing_chessBoard(int cols,int queens[][8])
{
int rows_note = 1;
int num = 0;
for( int i = 0; i < MAX_SIZE; i++)
{
if( check_queen(cols,i,queens) ){
//当一行放置了大于一个的皇后的时候,移除掉前面放置的皇后。因为是按行的顺序放置的,所以说,行的检查是可以避免的
if(num > 0){
queens[cols][rows_note] = 0;
num--;
}
queens[cols][i] = 'Q';
rows_note = i;
num++;
//成功放置一个皇后,则进入下一行的皇后检查,如果已经是最后一行就清空本层皇后,然后返回
if( cols == MAX_SIZE - 1)
{
print_chessBoard(queens);
queens[cols][rows_note] = 0;
return;
}
travelsing_chessBoard( cols + 1,queens);
}
}
//如果已经达到了行的末尾,那么本行中的皇后清0,为返回上一层的枝做空间清理
queens[cols][rows_note] = 0;
return;
}
int main()
{
int queens[MAX_SIZE][MAX_SIZE] = {0};
travelsing_chessBoard(0,queens);
printf("\n共有%d张棋谱",Chess_manual);
}
现在再来看这三句注释,其实就是被我们忽视的死路处理:
- 当一行放置了大于一个的皇后的时候,移除掉前面放置的皇后。因为是按行的顺序放置的,所以说,行的检查是可以避免的
- 成功放置一个皇后,则进入下一行的皇后检查,如果已经是最后一行就清空本层皇后,然后返回
- 如果已经达到了行的末尾,那么本行中的皇后清0,为返回上一层的枝做空间清理
程序结果如图: