从迷宫到八皇后问题认识递归与回溯
迷宫问题
迷宫大家都很熟悉,给定一个起点,一个终点,中间有各种复杂的通路,从起点走到终点就算是走出了迷宫。
那么如何使用计算机机算出一条迷宫的走法呢?
首先需要先在计算机中模拟出一个迷宫的样子,之前我们提到过的二维数组可以用来表示一个平面,用作迷宫的地图是非常合适的。
我们可以创建一个二维数组,用0表示路,1表示墙,即可表示任意方形的迷宫,下面是我在Java中随便创建的一个小迷宫,接下来我们将用他来完成对迷宫问题的解决
/**
* 创建迷宫,用二维数组存放返回
* @return 迷宫
*/
public static int[][] createMaze(){
//创建八行七列的地图
int[][] map = new int[8][7];
//用1表示墙,将四周围起来
for (int i = 0; i < 7; i++) {
map[0][i] = 1;
map[7][i] = 1;
}
for (int i = 0; i < 8; i++) {
map[i][0] = 1;
map[i][6] = 1;
}
//设置中间的墙
map[2][1] = 1;
map[2][3] = 1;
map[2][4] = 1;
map[3][4] = 1;
map[4][2] = 1;
map[4][3] = 1;
map[5][3] = 1;
map[5][5] = 1;
map[6][2] = 1;
return map;
}
打印一下迷宫看看效果
/**
* 打印迷宫(打印二维数组)
* @param map 二维数组
*/
public static void printMaze(int[][] map){
for (int[] row : map) {
for (int data : row) {
System.out.print(data+"\t");
}
System.out.println();
}
}
1是墙,0是路,没有什么问题,接下来开始分析如何用Java计算出一条通路。
我们可以让计算机尝试按照一种策略去穷举,也就是把能走的路全部走一遍。
我们发现每一个点尝试继续往下走,可以选择四个方向上、下、左、右
那么就要选定一种尝试的顺序策略,比如规定按照下、右、上、左的顺序
也就是说,走下一步的时候先尝试向下走,走不通再尝试向右走,走不通再尝试向上走,走不通再尝试向左走,都走不通说明只要来到这个点就已经是死路了,我们要回到上一个点再去尝试下一种方式
策略定下了,我们来分析什么样的路是走不通的路:
- 首先如果那个位置的数据为1也就是墙,走不通
- 如果这个位置是我们来时的位置,再走就成回头路了,也应该走不通
- 如果这个位置是以前尝试过的死路,也不必再走
综上我们发现,需要对走过的路也做一个记录,我们规定将走过的路标记为2,走过已经确定是死路的点标记为3
这时候,重点来了,体会用递归的方式来完成这种回溯,我们设计一个方法,让这个方法的功能是从当前点位开始,尝试找到一条通向重点的路,那么思路应该是先尝试向下走,走到下一个点,从下一个点再尝试走到再下一个点直到走通(到达终点)返回true,或者走不通(将走不通的点标记为3)返回false
可以这样想,我们用一个固定的走迷宫的方式(按顺序尝试),从一个点移动到了下一个点,还要再向下一个点移动,那么我们先不管我们是从哪里走过来的,就将当前所在这个点看成是一个新的起点再去按固定的走迷宫方式尝试下去。
解释起来有点抽象,看到这里应该是有点云里雾里,不知道如何去做,此时应该上代码了,结合上面的话已经代码注释来读一下代码,应该就能够理解了
我们就使用刚刚创建的那个迷宫,将[1,1]点作为总起点,[6,5]点作为终点来实现
/**
* 走迷宫
* 结束点写死,map[6][5]
* 约定,当地图[i][j]为0表示路没有走过,为1时表示墙,为2时表示通路可以走,3表示该位置已经走过但是不通
* 策略——>走迷宫时,下-右-上-左,走不通再回溯
*
* @param map 迷宫地图
* @param i 从哪里开始(起点的行索引)
* @param j 从哪里开始(起点的列索引)
* @return 是否能够找到通路
*/
public static boolean setWay(int[][] map,int i,int j){
//进入方法先判断当前点是不是终点,如果当前点就是终点,说明已经走通了,返回true
if (i == 6 && j == 5){
//已经是终点了,设置这个点走过了,返回true
map[i][j] = 2;
return true;
}
//没返回说明当前点不是终点,需要继续走
//再判断,当前点能不能走,如果当前点为0(1是墙,2是回头路,3是死路),表示这个点可以走
if (map[i][j] == 0) {
//路能走,就先将当前点设置为走过
map[i][j] = 2;
//然后按策略走一遍迷宫——>下->右->上->左
//先尝试向下走
// 把向下走的那个点再作为起点,调用自己这个走迷宫的方法
// 如果走通了就返回true(一层一层返回true最终返回到最外层)
// 如果走不通返回了false,会回到这里,接着去尝试向右走
// (false也是下面的很多个点一层一层返回到这里)
if (setWay2(map,i+1,j)){
return true;
}
//直到向下走不了,改向右走
else if (setWay2(map,i, j+1)){
return true;
}
//向右也走不通,改向上走
else if (setWay2(map,i-1,j)){
return true;
}
//向上也走不通,改向左走
else if (setWay2(map,i,j-1)){
return true;
}else {
//全都走不通,是死路,将当前点标记为3
map[i][j] = 3;
return false;
}
}
//如果当前点不为0,返回false
else {
return false;
}
}
由于传入方法的二维数组是引用类型,传来的是地址,所以方法中对这个数组的操作都会实际作用在数组上,方法结束后打印数据看看效果
public static void main(String[] args) {
//创建迷宫
int[][] maze = createMaze();
//使用递归回溯走迷宫
setWay(maze,1,1);
//输出尝试走过的地图
printMaze(maze);
}
我们看到算法是正确的,找到了一条标记为2的通路,从[1,1]走到[6,5],还有一些标记为3的也都确实是走不通的死路。
八皇后问题
八皇后问题表述为:在8×8格的国际象棋上摆放8个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。
高斯认为有76种方案。1854年在柏林的象棋杂志上不同的作者发表了40种不同的解,后来有人用图论的方法解出92种结果。如果经过±90度、±180度旋转,和对角线对称变换的摆法看成一类,共有42类。
但我们这里暂时先不讨论旋转和对称变换。
如图所示就是一种八皇后的摆放方式
如何使用计算机算出一共有多少摆放方式,并且将所有摆放方式列举出来呢?
这里依然是采用一种递归+回溯的思路,先分析一下:
首先分析可以按行考虑或按列考虑,比如按行考虑,就是说第一行放了皇后,下一个皇后就只能放第二行,再下一个就只能放第三行,每一行都去尝试皇后可以放在那一列来进行递归
一个棋盘按说可以存进一个二维数组,但是皇后的摆放有每一行只能有一个的限制,所以可以用一个一位数组即可,数组下标表示第几行,具体数据记录在那一列
那么就可以先将第一个皇后放在第一行第一列,然后去考虑第二行皇后可以放在哪里、第三行...一直到第八行
我们的方法可以设计为当前正在放第几个皇后,但是要记录好之前的皇后都怎么摆的
假设我们正在放第n个皇后
首先需要一个数组arr[]来保存八个皇后的摆放位置,一个变量count记录一共有多少种摆法。
然后进行如下步骤:
- 在第n行循环将皇后尝试摆放在第1到第8列
- 尝试第i列时,将皇后放进去(arr[n]=i),再循环去分析和前n-1行的皇后是否有冲突
- 如果没有冲突说明前n行摆的是可以的,接着去调用我们自己,摆第n+1个皇后,无论后面的皇后摆的怎么样,到方法执行完,还是要接着去尝试第i+1列
- 如果冲突直接继续去尝试第i+1列
- 直到八列都尝试完,说明第n个皇后后续所有情况已经穷尽了,也就是对我们的上一层来说,步骤c摆放第n+1个皇后方法执行完毕了,如果我们就是最外层,说明所有情况都穷尽了
- 而摆第n个皇后的时候,要进行边界判断,如果n变成8了,说明arr[0]到arr[7]八个皇后都放好了,此时数组arr中完整的保存了一种皇后的摆法,将它打印或记录下来,让解法总数+1
- 还有一个重点,就是如何判断冲突,行冲突不用判断因为方法就是从行出发,每行一定只有一个,还需判断列冲突和对角线冲突,列冲突非常简单,判断数组中第n个和前n-1个有没有相同即可,判断对角看似非常复杂,但只要借助一个叫做斜率的概念即可简单解决,观察可发现,如果是在一个对角线,这个对角线一定属于一个正方形,正方形对角线斜率为1,也就是第n个皇后和前面第i个皇后,如果在一个对角线,那么n-i == arr[n]-arr[i]。
八皇后比起刚刚迷宫只找一条通路应该是要难理解一些的,这种递归+回溯的方式,确实需要一定的想象力,下面在结合注释阅读一下代码,慢慢体会这种算法
package com.zwx.recursion;
import java.util.Arrays;
/**
* 八皇后问题(递归回溯)
*
* @author coderZWX
* @date 2022-05-31 15:41
*/
public class EightQueens {
//定义一个max表示共有多少个皇后
private int maxQueensCount = 8;
//定义一个数组,保存皇后放置位置的结果,比如arr={0,4,7,5,2,6,1,3}
private int[] array = new int[maxQueensCount];
//解法总数
private int count;
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
EightQueens queens = new EightQueens();
queens.check(0);
System.out.println(queens.count);
System.out.println("用时(毫秒):"+(System.currentTimeMillis()-startTime));
}
/**
* 放置第n个皇后
* @param n n表示数组下标,第一个皇后其实n为0
*/
private void check(int n){
//判断n是否越界,如果越界到第九个,说明已经八个皇后已经放好了
if (n == maxQueensCount){
//打印一个结果
System.out.println(Arrays.toString(array));
//记录结果数量加一
count++;
return;
}
//依次放入皇后并判断是否冲突
for (int i = 0; i < maxQueensCount; i++) {
//先把当前皇后n,放到该行的第i列
array[n] = i;
//判断是否冲突
// 不论是否冲突,都会最终i++接着执行下一次循环
// 如果冲突就直接进下一次循环接着尝试下一列
// 不冲突在放完后面皇后之后也会进下一次循环尝试下一列
if (judge(n)){
//不冲突接着放n+1个皇后
check(n+1);
}
}
//如果出了这个大循环
// 说明第n个皇后在第n行放哪里都不行,也就是前面的摆法都是错的
// 方法到这里就结束了,如果是内层,它的外层还会在外层的for接着循环
}
/**
* 查看当放置第n个皇后时,是否冲突
*
* @param n 第n个皇后
* @return 是否冲突
*/
private boolean judge(int n){
for (int i = 0; i < n; i++) {
//1.array[i] == array[n] 同一列冲突
//2.Math.abs(n-i) == Math.abs(array[n]-array[i]) 同一斜线冲突(相当于判断了两点间斜率为1)
if (array[i] == array[n] || Math.abs(n-i) == Math.abs(array[n]-array[i])){
return false;
}
}
return true;
}
}
根据上面代码,最终打印出了所有情况,count也就是92,没有问题
小练习——迷宫问题的最短路径
我们的迷宫问题,为了循序渐进,在前面的算法中只是找到了其中一条通路就结束了,所以理解起来比八皇后这个简单了不少,相当于八皇后问题只找到一种摆放方式。
那么现在经过了这两个算法的学习,结合迷宫问题找一条通路的方法、以及八皇后问题的思路,是否可以自行尝试写一个算法,穷举一下迷宫的所有通路,记录通路数量,并找到其中的最短路径
可评论留言获得小练习的实现(附详细的注释讲解)