
在上一篇推文《数据结构课程设计——迷宫求解(二)》中,我们已经了解过了如何生成一个随机迷宫,使用的算法是随机 Prim 算法,在最后使用可视化的方式展示了某次算法执行后生成的迷宫。一些基本的事情都已经处理好了,接下来将开始介绍课设的主要内容——寻找出路。
我们简单的规定左上角的一个空白格子为起点,右下角的一个空白格子为终点,使用算法找出一条从起点到终点的线路或者得出没有线路的结论。
方法很多,本系列将依次介绍以下四种方法,本文只介绍第一种。
深度优先搜索求解迷宫出路
广度优先搜索求解迷宫出路
A 星算法求解迷宫出路
通过预处理的方式求解迷宫出路
0 1深度优先搜索深度优先搜索在前面的推文中已经介绍过了,《算法学习——递归与DFS》。当时使用的是递归的方式实现深度优先搜索,而课设规定只允许使用非递归算法,这也算是一个小的挑战,实际上实现起来也并不难。
在正式开始介绍 DFS 求解迷宫出路之前,和上一篇推文一样,先来一个题外话,我们先看看如何对一棵二叉树进行先序遍历。
0 2二叉树的先序遍历在介绍二叉树的先序遍历、中序遍历与后序遍历时,只介绍了递归的写法,详情可以参考这篇推文《Python数据结构——树(二)》,在文末就介绍了这三种遍历方式,如果使用递归方法实现将十分简单。需要说明的是,对二叉树的先序遍历就相当于是在对其进行深度优先搜索,总是要“一条道走到黑”到达叶子结点之后才回退。所以我们理解好如何将二叉树的先序遍历使用非递归方法实现,再去看迷宫的深度优先搜索求解就会简单很多。
二叉树的非递归先序遍历需要解决的一个问题是,如何实现回溯,回溯也就是回到距离当前最近的上一个状态,过去的状态的有很多个,我们怎么能够保证拿到的恰好就是上一个状态呢?答案是使用栈。栈的先进后出特点,能够让栈顶始终是当前状态的上一个状态,这样就能够方便回溯了。
先序遍历非递归思路如下(暂不考虑树为空树的情况):
当前结点不为空,访问当前结点
当前结点右子结点不为空,右子结点入栈
当前结点右子结点为空,不考虑,直接进入下一步
向左子结点移动,p = p.left,重复上述步骤。
若当前结点为空,栈不为空,栈顶弹出,实现回溯
p = stack.pop()
若当前结点为空,栈也为空,遍历结束
思路其实很简单的,直接贴上代码实现。
def preOrder(node):
stack = Stack() # 实例化一个栈的对象
while node is not None:
print(node.val) # 访问当前结点
if node.right is not None: # 当前结点右子不为空则入栈
stack.push(node.right)
node = node.left # 向左子结点移动
if node is None and not stack.isEmpty(): # 当前结点为空但栈不为空,回溯
node = stack.pop()
完全按照上述思路写的,这里 while 只判断了 node 不为空,没有判断栈是否为空,理由是如果内部的第二个 if 没有执行,而 node 却是 None,就足以说明栈为空,应当结束循环,否则栈不为空,退栈操作必定导致 node 不为空,循环应当继续。
关于二叉树的深度优先搜索,或者说是先序遍历,就先介绍到这里,在开始下面的内容之前,我们先来根据已有知识思考下,可以怎样实现非递归版本的深搜求解迷宫出路。
二叉树一个结点最多只有左右两个子结点,而迷宫中一个点可以有上下左右四个方向(其中一个是来路,不应当走的)
二叉树的深搜可以用栈实现回溯,迷宫的深搜也可以仿照这个思路使用栈完成回溯
为了避免向四个方向试探过程中走了重复的路线,需要将访问过的结点特殊标记。
二叉树遍历结束是把所有点都访问一遍,而迷宫问题结束条件是到达终点。
0 3深度优先搜索求解迷宫出路在开始之前先补充一个 Java 中让程序休眠一定毫秒数再继续执行的方法,目的是为了能看到动态过程,否则整个迷宫刚刚生成,搜索结果就差不多已经出来了,不便于观察。
// 休眠指定毫秒数
public static void sleep(long xms){
try {
Thread.sleep(xms);
} catch (Exception e) {
}
}
接下来直接贴代码,这里使用的栈,是 Java 中自带的,当然也可以自行实现,实现起来也并不难。
下面直接贴代码,这里面使用到了图形界面的部分,如果不了解图形界面或对此不感兴趣,可以将带有 DrawRect 的代码注释掉,最后将函数返回的结果进行打印即可获得一个三元组形式的解,Node 类的实现在上一篇推文中有介绍,此处不多解释。
// 深度优先搜索,传入参数分别为迷宫矩阵,起点、终点坐标,迷宫等级
public static Stack findWayByDFS(int maze[][], int start_x, int start_y, int end_x, int end_y, int level){
Stack stack = new Stack();
Node node;//用于记录三元组
boolean moved;
boolean find = false; // 用于记录是否查找到出口
while(true) {
moved = false;
sleep(100L);
if (start_x - 1 >= 0 && maze[start_x - 1][start_y] == 0) { // 向上走未越界且有路
node = new Node(start_x, start_y, "Up");
stack.push(node);
DrawMaze.drawMaze.drawRect(start_x, start_y, level, -1);
maze[start_x][start_y] = -1; // 走过这里
start_x = start_x - 1;
moved = true;
}
if (start_x + 1 1 && maze[start_x + 1][start_y] == 0) { // 向下走未越界且有路
node = new Node(start_x, start_y, "Down");
stack.push(node);
DrawMaze.drawMaze.drawRect(start_x, start_y, level, -1);
maze[start_x][start_y] = -1; // 走过这里
start_x = start_x + 1;
moved = true;
}
if (start_x == end_x && start_y == end_y) { // 到达终点,在此处判断是为了避免向左下角走而经过终点却不停止的情况
stack.push(new Node(start_x, start_y, "END"));
DrawMaze.drawMaze.drawRect(start_x, start_y, level, -1); // 将该点描上颜色
find = true;
break;
}
if (start_y - 1 >= 0 && maze[start_x][start_y - 1] == 0) { // 向左走未越界且有路
node = new Node(start_x, start_y, "Left");
stack.push(node);
DrawMaze.drawMaze.drawRect(start_x, start_y, level, -1);
maze[start_x][start_y] = -1; // 走过这里
start_y = start_y - 1;
moved = true;
}
if (start_y + 1 1 && maze[start_x][start_y + 1] == 0) { //向右走未越界且有路
node = new Node(start_x, start_y, "Right");
stack.push(node);
DrawMaze.drawMaze.drawRect(start_x, start_y, level, -1);
maze[start_x][start_y] = -1; // 走过这里
start_y = start_y + 1;
moved = true;
}
if (start_x == end_x && start_y == end_y) { // 到达终点
stack.push(new Node(start_x, start_y, "END"));
DrawMaze.drawMaze.drawRect(start_x, start_y, level, -1); // 将该点描上颜色
find = true;
break;
}
if (!moved) { // 若未进行移动,说明陷入死胡同
if (!stack.isEmpty()) { // 未达终点且栈不为空
// 考虑将端点描上颜色
if(maze[start_x][start_y]!=-1) {
DrawMaze.drawMaze.drawRect(start_x, start_y, level, -1);
sleep(120L);
}
DrawMaze.drawMaze.drawRect(start_x, start_y, level, 0); // 将端点复原
maze[start_x][start_y] = -1; // 将这一点标记为死路
// 弹栈,回溯
node = (Node)stack.pop();
start_x = node.x;
start_y = node.y;
} else {
find = false;
break;
}
}
}
return find? stack: null;
}
对 Java 熟悉程度不够,代码或许还可以优化下,但目前只能写成这样,主要还是先弄清楚整个算法流程。函数的参数列表中,level 这个参数是迷宫的复杂等级,目前我们只考虑最简单的一级迷宫,实际上我最后完成的课设设计了三种等级的迷宫。
代码结尾处的 return 语句,使用了三目运算符,如果有 C/C++ 基础应该能够看懂,翻译成 Python 如下
return find if not stack.isEmpty() else None
代码中的主体部分就是向四个方向试探的四个 if 语句,其中在向上下方向试探结束后,判断了一次是否到达终点,在向左右方向试探结束后,再次判断了一次是否到达终点,这两次判断是我在实际运行中发现了问题后进行的改进措施。例如终点的上方和左方都还有路的情况,会出现深搜到达了终点,但是不会停下,会继续探测,直到撞墙回溯时回退到终点才会停下。
代码只是看起来较多,实际不难,其中我们将试探过的点标记成 -1,这样可以避免在两个点之间不断徘徊,造成死循环。即不往来的方向走。
最后附上运行演示。
这张图也就是上面说的为什么要判断两次终点的那种情况,否则到达了终点还会继续向左走,直到撞墙再回退回来才会停止。
从上面的演示动图可以看出来深搜的效率并不高,而且深搜是盲目的一条条路去测试能否到达终点,时间复杂度较高,不过如果多次运行程序,也会发现,有时候少数几次试探就能到达终点,这也就导致了有时候会出现深搜比较快的情况,不过毕竟是少数情况,总的来说深搜还是较慢的。
END在最后Last but not least
本文介绍了深度优先搜索求解迷宫出路,通过讲解二叉树的先序遍历非递归程序引入迷宫的非递归深搜程序,两者有相似之处也有不同之处,二叉树一个结点最多两个分叉,而迷宫中一个点却有四个方向,但两者都可以使用栈来实现非递归程序。
思路很简单,感兴趣的读者可以自行尝试,在下一篇推文中,将继续介绍第二种求解迷宫出路的方法——广度优先搜索。
往期 精彩回顾Python数据结构——树(五)
数据结构课程设计——迷宫求解(一)
数据结构课程设计——迷宫求解(二)