八皇后问题(Eight queens)
题目要求如下:
在8x8格的国际象棋上摆放8个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。在西洋棋中的皇后可以在没有限定一步走几格的前提下,对棋盘中的其它棋子直吃、横吃及对角斜吃(左斜吃或右斜吃皆可),只要是后放入的新皇后,放入前必须考虑所放位置直线方向、横线方向或对角线方向是否已被放置旧皇后,否则就会被先放入的旧皇后吃掉。
如下所示为8皇后的一组解:
问题分析
首先很容易想到使用穷举法来解决这个问题,即把网格中放置皇后的所有情况穷举出来,这样的话,对于八皇后问题有88=16777216种情况,这无疑已经是一个很大的计算量了,那有没有别的方法解决呢?
回溯法
:首先,我们在棋盘中第一列放置第一个皇后,然后在第二列第一个位置开始判断,依次向下找到一个安全的位置放置第二个皇后,继续在第三列依次找到一个安全的位置放置第三个皇后,依次类推,如果在某一列,如在第五列依次寻找位置时8个位置均不能安全的放置皇后,那么就回溯到第四列,重新在第四列找到另一个安全的位置,再继续寻找下一列。
代码实例
实例一
回溯法
:
package Data_struct;
/**
* @Author : goldsunC
* @Date : 2020/10/05/
* @Email : 2428022854@qq.com
* @Blog : https://blog.csdn.net/weixin_45634606
*/
public class QueenEight2 {
public static void main(String[] args) {
new QueenEight2().solve();
}
private static final int N = 8;
private int[] y; // 记录每列上皇后放的位置
int count = 0; // 解的个数
public void solve() {
count = 0;
y = new int[N+1]; // 初始化数组
int x = 1;
while (x>0) {
y[x]++; //为当前x位置找一个皇后的位置
while ((y[x]<=N)&&(!check(x)))
y[x]++; //找到合适的位置
if (y[x]<=N) {
if (x == N) { // 如果找到了一个放置皇后的完整方案,解+1,打印结果
count++;
print();
}else x++; // 还没找到完整方法,继续寻找下一个皇后位置。
}else {
// 所有位置均不安全,回溯。
y[x] = 0;
x--;
}
}
}
// 测试合法性
private boolean check(int k) {
for (int j=1;j<k;j++)
if ((Math.abs(k-j)==Math.abs(y[j]-y[k])) || (y[j] == y[k]))
return false;
return true;
}
// 显示结果
private void print() {
System.out.println(count);
for (int i=1;i<=N;i++) {
for (int j=1;j<=N;j++)
if (j == y[i])
System.out.print("x"); //如果该位置放了皇后则显示x
else
System.out.print("o");
System.out.println();
}
}
}
代码不长,并且核心的也就那么一二十行,但是如果你是个算法小白的话,自己还是不容易看懂这几行代码的。下面我结合代码来分析一下回溯法
。
简单从三个方面介绍上面这个示例代码:
变量声明
测试合法性(即这个位置是否安全)
算法结构
变量声明
N
:变量N指的是棋盘为8x8,即代表8皇后问题。其中在solve
方法部分声明y时用的N+1
,这是因为Java中数组下标从0开始,为了便于理解我们声明有9列,但是只使用后8列,则下标就是1-8了。y[]
:y数组存放的是每列上皇后的位置,注意是列,如果以文章开头那组解为例,则y应该为[x 1 5 8 6 3 7 2 4],注意y有9列,第一列我用x代表我们没有使用。count
:记录解的个数。每找到一组解,count+1。
测试合法性
我们使用check(int k)
方法来测试当前位置是否安全,要注意的是,8皇后问题应该是一个8x8的棋盘,每个位置都有横纵坐标,但是这个方法只有一个参数k,这个k由solve
方法中的x传入,指的是棋盘中的列
。因为棋盘中每一列只能有一个皇后,因此y[x]
则代表x
列皇后的位置。
那么我们如何知道当前位置是否安全?
根据题意,只要当前位置的同一行、同一列、左对角线、右对角线上面没有皇后则此位置是安全的
。
对于当前位置,其有一个坐标(x,y),对于之前放置过的皇后,每一个也有其坐标(a,b)。
判断是否与其它皇后同一行、同一列
:我们是按照一列一列的放置皇后,每列放置一个后即放置下一列,因此同一列上不可能出现其它皇后,那只需要判断当前位置的同一行是否有旧皇后即可,我们之前说过,k
代表的是列,那什么代表行呢?对,是y[k]
,因此只需要判断y[k]
是否等于a
即可,我们需要保证的是当前位置的同一行不出现其它皇后,那么只需要遍历
之前放置所有皇后的横坐标是否等于y[k]
即可。判断是否与其它皇后处在同一对角线
:怎么样才算处在一条对角线呢?这个时候我们就需要分析一下对角线上面的点的特征了,假如当前位置的坐标是(x,y),a为一个格子的单位长度,那么与当前位置同处左
对角线的任一点的坐标一定是(x+a,y+a)或(x-a,y-a),而与当前位置同处右
对角线的任一点的坐标一定是(x-a,y+a)或(x+a,y-a),仔细观察下容易发现,左右对角线上的点横坐标有两种情况(x+a)或(x-a),纵坐标为(y+a)或(y-a),对角线上的点与当前位置无非是加上或者减去一个单位长度而已,如果将当前位置横纵坐标分别与对角线上点的横纵坐标做差值,那么横坐标即(x-(x+a))或者(x-(x-a))即正负a,同理发现纵坐标差值也为正负a,如果给正负a加上绝对值呢?就变成了正a,这个时候横纵坐标分别的差值是相等的。因此只要当前位置的横纵坐标与之前每一个皇后的横纵坐标之差不相等,即可说明当前位置安全。
算法结构
整个算法中包含了三个方法(除去main),分别是solve
、check
、print
,其中check已经说过,print较简单不再赘述,solve方法即算法求解的方法:
- 首先初始化参数部分
- 从第一个列开始放皇后
- 如果当前位置安全且在棋盘内,则此列放置好皇后
- 如果8列全部放好,解+1,打印结果,此时
最后一列的皇后重新向后找解(目的是找到8皇后中的所有解)
- 如果8列没放好,就继续放下一列
- 如果遍历当前列所有位置后均不安全,回溯,去重新放置上一列的皇后。
实例二
回溯+递归
:
package Data_struct;
import java.io.IOException;
/**
* @Author : goldsunC
* @Date : 2020/10/05/
* @Email : 2428022854@qq.com
* @Blog : https://blog.csdn.net/weixin_45634606
*/
public class QueenEight {
static int TRUE = 1,FALSE = 0,EIGHT = 8;
//存放8个皇后的列位置
static int[] queen = new int[EIGHT];
//计算共有几组解的总数
static int number = 0;
//构造函数
QueenEight() {
number = 0;
}
public static void PressEnter() {
char tChar;
System.out.println("\n\n");
System.out.println("...按下Enter键继续...");
try {
tChar = (char)System.in.read();
}catch (IOException e) {}
}
/**
* 决定皇后存放的位置
*/
public static void decide_position(int value) {
int i = 0;
while (i<EIGHT) {
if (attack(i,value)!=1) {
queen[value] = i;
if (value == 7)
print_table();
else
decide_position(value+1);
}
i++;
}
}
public static int attack(int row,int col) {
int i=0,atk = FALSE;
int offset_row = 0,offset_col = 0;
while ((atk!=1) && i<col) {
offset_col = Math.abs(i - col);
offset_row = Math.abs(queen[i] - row);
//判断两皇后是否在同一列或在同一对角线
if ((queen[i] == row) || (offset_row == offset_col))
atk = TRUE;
i++;
}
return atk;
}
public static void print_table() {
int x=0,y=0;
number += 1;
System.out.print("\n");
System.out.print("八皇后问题的第"+number+"组解\n\t");
for (x=0;x<EIGHT;x++) {
for (y=0;y<EIGHT;y++)
if (x == queen[y])
System.out.print("<*>");
else
System.out.print("<->");
System.out.print("\n\t");
}
PressEnter();
}
public static void main(String[] args) {
QueenEight.decide_position(0);
}
}
这个方法核心也是回溯,但是结合了递归的方法来实现,可能比刚才那个纯递归的方法理解起来稍微难一点,下面我也来分析一下这种方法。
简单从四个方面介绍以上示例代码:
变量声明
测试合法性
算法结构
递归分析
变量声明
这个方法中用的变量和上个方法不一样,仔细观察的同志应该就发现了,回溯法
中的变量包括方法都是实例变量或者实例方法
,而回溯递归法
中的变量都是类变量(或者说静态变量吧)或者类方法
,实际上区别不大哈,从main
函数中的new QueenEight2().solve();
和QueenEight.decide_position(0);
就能看出来区别哈,多余的就不再讲了。
TRUE和FALSE
:这两个呢其实是0和1,不是布尔值,算法中用0和1代表是否收到攻击。
EIGHT
:顾名思义就是8哈哈哈哈,代表8皇后。
queen
:存放8个皇后的列位置,注意这个数组长度是8,要从0开始算喽。
number
:解的个数。
总的来说两种算法的变量方面差别不大。
测试合法性
回溯递归法
中测试合法性使用的是attack(int row,int col)
函数,这个函数有两个变量,row和col分别代表当前位置的横纵坐标,测试原理与回溯法
一样,唯一不同就是传入了两个参数,如何测试合法性请参考回溯法
。
算法结构
算法中除了main之外包含了四个方法,分别是:PressEnter
、decide_position
、attack
、print_table
,其中attack原理已经讲过不再赘述,print_table用来打印结果比较简单不再赘述,PressEnter更加简单不再赘述,decide_position与solve一样也是为求解的方法:
i
代表的是行,value
代表的是列,注意QueenEight.decide_position(0);
传入的是0,代表从第1列开始放置皇后,即按照列顺序放置皇后,与solve方法一样。- 当列确定后,从第一行开始找位置,判断是否安全。
- 如果安全则放置皇后,同时判断8列是否放置好。
- 如果8列全部放置好,打印结果,同时
当前列(也就是第8列,继续向后寻找位置,继续找解)
。 - 如果8列没有全部放置好,
套娃
放置下一列。
注意这个套娃
就是递归,大家很容易看出来,这个decide_position
和之前的solve
函数相比,简短了许多,结构也清晰了很多,这就是递归的魅力,可就是不好理解。。。下面给大家仔细讲下这个套娃:
我们从第一列说起,假如我们在第一列已经找好了放置皇后的位置,这个时候呢不是在当前棋盘的下一列去找位置了,而是重新拿出了一个新棋盘,因为变量都是static
的原因,新棋盘之前的皇后位置和老棋盘一模一样,然后呢我们就在新棋盘上去找下一列的皇后位置,找呀找呀,找到了,那么再拿出新的棋盘去放下一个皇后,哎,如果8个位置都不行,那怎么办?把新棋盘扔了!回溯!去找上一个位置的皇后,可棋盘扔了在哪找皇后呢?在老棋盘上面找。
总的来说,这个算法的递归就是这样搞的,但你可能有疑惑,为什么要这么搞?不知道你有没有观察到,在solve
方法中有x--
这个操作,我们知道x
代表列,与decide_position
中的value
含义一样,但是没有value--
,那么既然是回溯,你的列从不倒退,怎么回溯?答案就是通过扔棋盘的方式回溯!每一个棋盘都有一个value
值,这个值随着新棋盘的诞生而增加,那把新棋盘扔了,用老棋盘不就相当于value--
了吗?因为自学的原因,当时我想了好长时间才想通这个看懂代码,那么你理解了吗?
总结
今天用了两种方法求解了8皇后问题,分别是纯回溯法
和回溯+递归
法,两种方法其实大同小异,在测试合法性方面原理一样,实现略微不同,主要思想都是回溯,说到底,回溯其实也是穷举法,只不过是一种优化的穷举法,那两者主要不同在哪呢?举个简单的例子就能明白:
回溯和穷举现在到了一个迷宫,面前有10条路,他们得去找到出口,回溯和穷举同时进了同一个路口,进去后俩人一眼望去,前面有堵墙,这时候回溯说:“穷举老弟,前面有堵墙哎,是死路,咱们回去吧,去下一条路。”穷举脑子不好使,比较笨,说:“死路是啥,我过去看看~”于是,回溯回去找下一条路了,穷举继续顺着原路走了。你说,谁能先出去?