本篇文章上半部分来自于公众号 程序员小灰
八皇后问题是一个古老而著名的问题,是回溯算法的的典型例题。该问题是十九世纪著名的数学家高斯1850年提出:在8´8格的国际象棋棋盘上,安放八个皇后,要求没有一个皇后能够“吃掉”任何其他一个皇后,即任意两个皇后都不能处于同一行、同一列或同一条对角线上,求解有多少种摆法。
高斯认为有76种方案。1854年在柏林的象棋杂志上不同的作者发表了40种不同的解,后来有人用图论的方法得出结论,有92种摆法。
国际象棋中的皇后,可以横向、纵向、斜向移动。如何在一个8X8的棋盘上放置8个皇后,使得任意两个皇后都不在同一条横线、竖线、斜线方向上?
让我们来举个栗子,下图的绿色格子是一个皇后在棋盘上的“封锁范围”,其他皇后不得放置在这些格子:
下图的绿色格子是两个皇后在棋盘上的“封锁范围”,其他皇后不得放置在这些格子:
如何解决八皇后问题?
所谓递归回溯,本质上是一种枚举法。这种方法从棋盘的第一行开始尝试摆放第一个皇后,摆放成功后,递归一层,再遵循规则在棋盘第二行来摆放第二个皇后。如果当前位置无法摆放,则向右移动一格再次尝试,如果摆放成功,则继续递归一层,摆放第三个皇后......
如果某一层看遍了所有格子,都无法成功摆放,则回溯到上一个皇后,让上一个皇后右移一格,再进行递归。如果八个皇后都摆放完毕且符合规则,那么就得到了其中一种正确的解法。
说起来有些抽象,我们来看一看递归回溯的详细过程。
1.第一层递归,尝试在第一行摆放第一个皇后:
2.第二层递归,尝试在第二行摆放第二个皇后(前两格被第一个皇后封锁,只能落在第三格):
3.第三层递归,尝试在第三行摆放第三个皇后(前四格被第一第二个皇后封锁,只能落在第五格):
4.第四层递归,尝试在第四行摆放第四个皇后(第一格被第二个皇后封锁,只能落在第二格):
5.第五层递归,尝试在第五行摆放第五个皇后(前三格被前面的皇后封锁,只能落在第四格):
6.由于所有格子都“绿了”,第六行已经没办法摆放皇后,于是进行回溯,重新摆放第五个皇后到第八格。:
7.第六行仍然没有办法摆放皇后,第五行也已经尝试遍了,于是回溯到第四行,重新摆放第四个皇后到第七格。:
8.继续摆放第五个皇后,以此类推......
八皇后问题的代码实现?
解决八皇后问题,可以分为两个层面:
1.找出第一种正确摆放方式,也就是深度优先遍历。
2.找出全部的正确摆放方式,也就是广度优先遍历。
在研究代码实现的时候,我们需要解决几个问题:
1.国际象棋的棋盘如何表示?
很简单,用一个长度是8的二维数组来表示即可。
static final int MAX = 8;
int qipan[][] = new int[MAX][MAX];
由于这里使用的是int数组,int的初始值是0,代表没有落子。当有皇后放置的时候,对应的元素值改为1。
在这里,二维数组的第一个维度代表横坐标,第二个维度代表纵坐标,并且从0开始。比如qipan[3][4]代表的是棋盘第四行第五列格子的状态。
2.如何判断皇后的落点是否合规?
定义一个check方法,传入新皇后的落点,通过纵向和斜向是否存在其他皇后来判断是否合规。
// 检查棋盘
boolean cherk(int x, int y) {
// 检查
for (int i = 0; i < y; i++) {
// 检查纵向
if (qipan[x][i] != 0)
return false;
// 检查左斜
if (x - 1 - i >= 0 && qipan[x - 1 - i][y - 1 - i] != 0)
return false;
// 检查右斜
if (x + 1 + i < MAX && qipan[x + 1 + i][y - 1 - i] != 0)
return false;
}
// 若可以放置则放置
return true;
}
3.如何进行递归回溯?
递归回溯是本算法的核心,代码逻辑有些复杂
PS:此处代码修改了,小灰的代码知识输出一个,本次修改完后输出92种
// 递归函数放置皇后
boolean setqueen(int y) {
// 行数超过8,则说明找出正确答案
if (y == MAX)
return true;
// 遍历当前行,逐一验证
for (int i = 0; i < MAX; i++) {
// 为当前行清零,防止出现脏数据
for (int x = 0; x < MAX; x++) {
qipan[x][y] = 0;
}
// 检查是否符合规则,符合则进一步递归
if (cherk(i, y)) {
qipan[i][y] = 1;
// 递归如果返回true说明下层找到解决,无需继续循环
if (setqueen(y + 1)) {
System.out.println("第"+ k + "个排列");
k++;
printqipan();
return false;
}
}
}
return false;
}
4.如何输出结果?
这个问题很简单,直接遍历二维数组并输出就可以。
PS:此处代码修改了,小灰的代码是输出二维数组,这个输出一个棋盘
void printqipan() {
// 遍历数组输出
for (int j = 0; j < MAX; j++) {
for (int i = 0; i < MAX; i++) {
if (qipan[i][j]!=0) {
System.out.print("●");
}else {
System.out.print("□");
}
}
System.out.println();
}
}
5.如何把这些方法串起来?
在main函数里分三步来调用:
第一步:初始化
第二步:递归摆放皇后
第三步:最后输出结果。
其中Queen是整个类的名字。
public class Main {
public static void main(String[] args) {
Queen queen = new Queen();
queen.setqueen(0);
}
}
最终输出如下:(图太长,就不粘贴了)
其实写到这,↑面的90%都是抄的,怎么算原创呢?
下面才是自己改的并写的。
java本来就是在小灰的基础上改的
附上java代码
public class Queen{
static final int MAX = 8;
int qipan[][] = new int[MAX][MAX];
int k=1;
// 检查棋盘
boolean cherk(int x, int y) {
// 检查
for (int i = 0; i < y; i++) {
// 检查纵向
if (qipan[x][i] != 0)
return false;
// 检查左斜
if (x - 1 - i >= 0 && qipan[x - 1 - i][y - 1 - i] != 0)
return false;
// 检查右斜
if (x + 1 + i < MAX && qipan[x + 1 + i][y - 1 - i] != 0)
return false;
}
// 若可以放置则放置
return true;
}
// 递归函数放置皇后
boolean setqueen(int y) {
// 行数超过8,则说明找出正确答案
if (y == MAX)
return true;
// 遍历当前行,逐一验证
for (int i = 0; i < MAX; i++) {
// 为当前行清零,防止出现脏数据
for (int x = 0; x < MAX; x++) {
qipan[x][y] = 0;
}
// 检查是否符合规则,符合则进一步递归
if (cherk(i, y)) {
qipan[i][y] = 1;
// 递归如果返回true说明下层找到解决,无需继续循环
if (setqueen(y + 1)) {
System.out.println("第"+ k + "个排列");
k++;
printqipan();
return false;
}
}
}
return false;
}
void printqipan() {
// 遍历数组输出
for (int j = 0; j < MAX; j++) {
for (int i = 0; i < MAX; i++) {
if (qipan[i][j]!=0) {
System.out.print("●");
}else {
System.out.print("□");
}
}
System.out.println();
}
}
}
Main代码
public class Main {
public static void main(String[] args) {
Queen queen = new Queen();
queen.setqueen(0);
}
}
接下来C代码(.cpp文件下运行)
#include<stdio.h>
#define MAX 8
int qipan[MAX][MAX];
int k=1;
bool cherk(int x,int y);
bool setqueen(int y);
void printqipan();
int main(){
printf("hello\n");
setqueen(0);
return 0;
}
// 检查棋盘
bool cherk(int x, int y) {
// 检查
for (int i = 0; i < y; i++) {
// 检查纵向
if (qipan[x][i] == 1)
return false;
// 检查左斜
if (x - 1 - i >= 0 && qipan[x - 1 - i][y - 1 - i] == 1)
return false;
// 检查右斜
if (x + 1 + i < MAX && qipan[x + 1 + i][y - 1 - i] == 1)
return false;
}
// 若可以放置则放置
return true;
}
// 递归函数放置皇后
bool setqueen(int y) {
// 行数超过8,则说明找出正确答案
if (y == MAX)
return true;
// 遍历当前行,逐一验证
for (int i = 0; i < MAX; i++) {
// 为当前行清零,防止出现脏数据
for (int x = 0; x < MAX; x++) {
qipan[x][y] = 0;
}
// 检查是否符合规则,符合则进一步递归
if (cherk(i, y)) {
qipan[i][y] = 1;
// 递归如果返回true说明下层找到解决,无需继续循环
if (setqueen(y + 1)) {
printf("第%d个排列\n",k);
k++;
printqipan();
return false;
}
}
}
return false;
}
void printqipan() {
// 遍历数组输出
for (int i = 0; i < MAX; i++) {
for (int j = 0; j < MAX; j++) {
printf("%d",qipan[i][j]);
}
printf("\n");
}
}
最后,是数据结构中的栈和此代码的结合,并规范使其在.c下运行
/**
*计算机科学与工程学院
*软件工程 数据结构实训第5组
* 八皇后
*要求:用到栈
*组长:莫言情难忘
*/
#include <stdio.h>
#include <stdlib.h>
#define STACK_INIT_SIZE 8
typedef int SElemType;
#define MAX 8
int qipan[MAX][MAX];
int k=1;
bool cherk(int x,int y);//检查棋盘
bool setqueen(int y);//回溯
typedef struct{
SElemType *base;
SElemType *top;
int stacksize;
}SqStack;
SqStack S;
//初始化
SqStack InitStack(SqStack *S)
{
S->base=(SElemType*)malloc(STACK_INIT_SIZE*sizeof(SElemType));
if(!S->base)
printf("申请失败");
else
{
S->top=S->base;
S->stacksize=STACK_INIT_SIZE;
}
return *S;
}
//进栈
push (SqStack *S,SElemType e)
{
if(S->top -S->base >=STACK_INIT_SIZE )
printf("栈满");
else
{
*S->top=e;
S->top++;
}
}
//出栈
pop(SqStack *S)
{
int e;
if(S->top == S->base )
printf("栈空");
else
{
--S->top ;
e=*S->top ;
//printf("出栈元素:%d \n",e);
}
}
//输出
void Outputstack(SqStack *S)
{
int shuzu[8];
int i=0;
SElemType *p;
//数组使其逆序
for(p=S->top-1;p>=S->base ;p--){
shuzu[i]=*p;
i++;
}
//输出
for(i=MAX-1;i>=0;i--)
{
switch(shuzu[i]){
case 1:printf("●□□□□□□□\n");break;
case 2:printf("□●□□□□□□\n");break;
case 3:printf("□□●□□□□□\n");break;
case 4:printf("□□□●□□□□\n");break;
case 5:printf("□□□□●□□□\n");break;
case 6:printf("□□□□□●□□\n");break;
case 7:printf("□□□□□□●□\n");break;
case 8:printf("□□□□□□□●\n");break;
}
// printf("%d ",shuzu[i]);
}
printf("\n");
}
void main()
{
//初始化栈
S=InitStack(&S);
setqueen(0);
}
// 检查棋盘
bool cherk(int x, int y) {
// 检查
for (int i = 0; i < y; i++) {
// 检查纵向
if (qipan[x][i] == 1)
return false;
// 检查左斜
if (x - 1 - i >= 0 && qipan[x - 1 - i][y - 1 - i] == 1)
return false;
// 检查右斜
if (x + 1 + i < MAX && qipan[x + 1 + i][y - 1 - i] == 1)
return false;
}
// 若可以放置则放置
return true;
}
// 递归函数放置皇后
bool setqueen(int y) {
// 行数超过8,则说明找出正确答案
if (y == MAX)
return true;
// 遍历当前行,逐一验证
for (int i = 0; i < MAX; i++) {
// 为当前行清零,防止出现脏数据
for (int x = 0; x < MAX; x++) {
qipan[x][y] = 0;
}
// 检查是否符合规则,符合则进一步递归
if (cherk(i, y)) {
qipan[i][y] = 1;
push(&S,i+1);
// 递归如果返回true说明下层找到解决,无需继续循环
if (setqueen(y + 1)) {
printf("第%d个排列\n",k);
k++;
Outputstack(&S);
pop(&S);//出栈,栈里8变7,Y为7
pop(&S);//出栈,栈里7变6,Y为7
return false;
}
}
}
pop(&S);
return false;
}
结语:此篇文章是我们数据结构课程实训结束后发的,我以前就用java写过八皇后问题的源码,这次实训,借用了小灰的思维,加以更改,顺利的完成了此次实训。