【参考1:Wiki百科-八皇后问题】(https://zh.wikipedia.org/wiki/八皇后问题)
【参考2:Wiki百科-回溯法】(https://zh.wikipedia.org/wiki/回溯法)
【参考3:八皇后问题求12个独立解】(http://tieba.baidu.com/p/101214429)
1.背景:
八皇后问题最早是由国际象棋棋手马克斯·贝瑟尔(Max Bezzel)于1848年提出。第一个解在1850年由弗朗兹·诺克(Franz Nauck)给出。并且将其推广为更一般的n皇后摆放问题。诺克也是首先将问题推广到更一般的n皇后摆放问题的人之一。
在此之后,陆续有数学家对其进行研究,其中包括高斯和康托,1874年,S.冈德尔提出了一个通过行列式来求解的方法[2],这个方法后来又被J.W.L.格莱舍加以改进。1972年,艾兹格·迪杰斯特拉用这个问题为例来说明他所谓结构化编程的能力[3]。他对深度优先搜索回溯算法有着非常详尽的描述2。八皇后问题在1990年代初期的著名电子游戏第七访客和NDS平台的著名电子游戏《雷顿教授与不可思议的小镇》中都有出现。
2.条件:
八皇后问题是一个以国际象棋为背景的问题:如何能够在8×8的国际象棋棋盘上放置八个皇后,使得任何一个皇后都无法直接吃掉其他的皇后?为了达到此目的,任两个皇后都不能处于同一条横行、纵行或斜线上,如下图。八皇后问题可以推广为更一般的n皇后摆放问题:这时棋盘的大小变为n×n,而皇后个数也变成n。当且仅当n = 1或n ≥ 4时问题有解。
3.解:
求解八皇后问题使用了回溯法和递归
-
回溯法是什么?
回溯法采用试错的思想,它尝试分步的去解决一个问题。在分步解决问题的过程中,当它通过尝试发现现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至是上几步的计算,再通过其它的可能的分步解答再次尝试寻找问题的答案。回溯法通常用最简单的递归方法来实现,在反复重复上述的步骤后可能出现两种情况:
找到一个可能存在的正确的答案
在尝试了所有可能的分步方法后宣告该问题没有答案
在最坏的情况下,回溯法会导致一次复杂度为指数时间的计算。 -
问题分析:
回溯法在进行完一次尝试后要恢复上一次的现场,所以就需要类似栈一样的东西
- 起初我的想法是设置一个8*8的数组代表棋盘,其中值为0代表不能放置皇后,为1代表可以放置皇后,但发现恢复现场非常麻烦,因为在某一位置放置皇后以后,其横行 竖行 斜线上的所有位置都需要置0,这就会导致某个位置因为两个不同的皇后导致置0了两次,如下红色位置为重复设置的位置
因此恢复现场会变得很复杂。 - 后面又想到了设置四个数组,分别代表8行 8列 15个捺斜线 15个撇斜线 四个数组,然后每放置一个皇后,四个数组分别有一个位置置0,但是同样的,行列可能会导致斜线位置并不都能放置皇后,因此也否定了。
- 最后想到的就是,首先每一行必须有一个皇后,设置一个有8个元素的数组queen,代表皇后的位置,queen[0]代表第1行皇后的列值,queen[1]代表第2行皇后的列值,以此类推,每一行皇后设置完后递归设置下一行的皇后,设置皇后时,从1列一直到8列依次尝试,每次尝试都要和这一行之前行的皇后进行"碰撞检测",如果有冲突,则停止此位置尝试,尝试下一个位置,直到尝试设置第9个皇后,这说明前面8个都放置好了,因此可能的布局+1,退出此次递归,继续尝试其他位置
- 具体c++代码 :
#include <iostream>
#include <iomanip>
#include "stdio.h"
#include "string.h"
#include "stdlib.h"
using namespace std;
int sum = 0;//总次数计数
void EightQueen(int *queen, int cnt)// queen:皇后位置数组,cnt:这次设置第cnt行
{
if(cnt == 8) // 尝试第9行,则可能性+1,停止递归
{
sum++;
}
for(int i = 0; i<8; i++) // 从第1列开始尝试到第8列
{
bool flag = true;
for(int j = 0; j<cnt; j++) // 与前面0-cnt-1行的皇后进行检测,因为j不会等于cnt,因此两个皇后必不在同一行
{
if(i == queen[j] || j-cnt == queen[j]-i || j+queen[j] == i+cnt) // i == queen[j]:是否在同一列,j-cnt == queen[j]-i:是否在同一捺斜线,j+queen[j] == i+cnt:是否在同一撇斜线
{
flag = false; // 有冲突则进行下个位置的尝试
break;
}
}
if(flag) // 无冲突则递归设置下一行的皇后
{
queen[cnt] = i; // 设置此行皇后位置
EightQueen(queen,cnt+1); // 递归
queen[cnt] = 0; // 恢复现场
}
}
}
int main()
{
int *queen = new int[8];
memset(queen,0,8*sizeof(int));
EightQueen(queen,0);
cout<<sum<<endl;
system("pause");
return 0;
}
- 八皇后问题扩展 - 互不相同解和独立解:
八个皇后在8x8棋盘上共有4,426,165,368( C 64 8 C^8_{64} C648)种摆放方法,但只有 92 92 92 个互不相同的解。如果将旋转和对称的解归为一种的话,则一共有 12 12 12 个独立解,通过92个解可以求得12个解,方法就是新求的解的对称、旋转的情况与前面的解进行比对,其中对称有四种,旋转有三种,加上自身,有八种情况,与这些情况比对,相同则略过,否则加入到结果数组中
要着重注意旋转和对称的代码实现,旋转包括90度 180度 270度旋转,但是180度旋转又是90度旋转后又旋转了90度的结果,因此,假设坐标从 ( 0 , 0 ) (0,0) (0,0)开始,某点 M M M坐标为 ( x , y ) (x,y) (x,y),旋转90度变为 ( 7 − y , x ) (7-y,x) (7−y,x),旋转180度变为 ( 7 − x , 7 − y ) (7-x,7-y) (7−x,7−y),旋转270度变为 ( y , 7 − x ) (y,7-x) (y,7−x),可以推出有0->90度推出其中规律,而对称包括水平、垂直、两个斜线对称,水平就是 ( x , 7 − y ) (x,7-y) (x,7−y),垂直就是 ( 7 − x , y ) (7-x,y) (7−x,y),左上右下对称就是 ( 7 − y , 7 − x ) (7-y,7-x) (7−y,7−x),左下右上对称就是 ( y , x ) (y,x) (y,x)。
c++代码:
#include <iostream>
#include <iomanip>
#include "stdio.h"
#include "string.h"
#include "stdlib.h"
#define OUTPUT 1 // 设置为0 不输出到文件,设置为1输出到文件
#if (OUTPUT==1)
FILE *fp;
#endif
using namespace std;
int result[100][8][8] = {0};
int sum = 0;//总次数计数
bool resCmp(int *queen) // 比较函数
{
for(int i = 0; i<sum; i++)
{
for(int j = 0; j<8; j++)
{
if(!memcmp(queen,result[i][j],8*sizeof(int))) // 内存比较
return false;
}
}
return true;
}
void EightQueen(int *queen, int cnt)// queen:皇后位置数组,cnt:这次设置第cnt行
{
if(cnt == 8) // 尝试第9行,则可能性+1,停止递归
{
if(resCmp(queen))
{
for(int j = 0; j<8; j++)
{
result[sum][0][j] = queen[j];
result[sum][1][7-queen[j]] = j; // 90度旋转
result[sum][2][7-j] = 7-queen[j]; // 180度旋转
result[sum][3][queen[j]] = 7-j; // 270度旋转
result[sum][4][j] = 7-queen[j]; // 左右调换
result[sum][5][7-j] = queen[j]; // 上下调换
result[sum][6][7-queen[j]] = 7-j; // 左上-右下调换
result[sum][7][queen[j]] = j; // 左下-右上调换
}
#if (OUTPUT==1)
fprintf(fp,"NO.%d:\n",sum+1);
for(int j = 0; j<8; j++)
{
for(int k = 0; k<8; k++)
{
if(result[sum][0][j] == k)
fprintf(fp,"■");
else
fprintf(fp,"□");
}
fprintf(fp,"\n");
}
fprintf(fp,"\n");
#endif
sum++;
}
}
for(int i = 0; i<8; i++) // 从第1列开始尝试到第8列
{
bool flag = true;
for(int j = 0; j<cnt; j++) // 与前面0-cnt-1行的皇后进行检测,因为j不会等于cnt,因此两个皇后必不在同一行
{
if(i == queen[j] || j-cnt == queen[j]-i || j+queen[j] == i+cnt) // i == queen[j]:是否在同一列,j-cnt == queen[j]-i:是否在同一捺斜线,j+queen[j] == i+cnt:是否在同一撇斜线
{
flag = false; // 有冲突则进行下个位置的尝试
break;
}
}
if(flag) // 无冲突则递归设置下一行的皇后
{
queen[cnt] = i; // 设置此行皇后位置
EightQueen(queen,cnt+1); // 递归
queen[cnt] = 0; // 恢复现场
}
}
}
int main()
{
int *queen = new int[8];
memset(queen,0,8*sizeof(int));
#if (OUTPUT==1)
fp = fopen("abc.txt","w");
#endif
EightQueen(queen,0);
cout<<sum<<endl; // 输出结果
#if (OUTPUT==1)
fclose(fp);
#endif
system("pause");
return 0;
}
具体结果如下:
- 八皇后问题推广 -
n
n
n皇后问题:
下表给出了n皇后问题的解的个数包括独立解U以及互不相同的解D的个数:
可见 n = 2 , 3 n=2,3 n=2,3时无解,如前面所说的当且仅当 n = 1 n = 1 n=1或 n ≥ 4 n ≥ 4 n≥4时问题有解
求n皇后问题两种解的代码很好写,只需把8皇后问题的代码中跟8有关的改成一个变量即可