数据结构课设 扫雷 c_数据结构课程设计——迷宫求解(三)

ffa59364c504611c52805e42d34de1ab.png前言

在上一篇推文《数据结构课程设计——迷宫求解(二)》中,我们已经了解过了如何生成一个随机迷宫,使用的算法是随机 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,这样可以避免在两个点之间不断徘徊,造成死循环。即不往来的方向走。

最后附上运行演示。

74808facc43728ae24b4fceeeb396dde.gif

这张图也就是上面说的为什么要判断两次终点的那种情况,否则到达了终点还会继续向左走,直到撞墙再回退回来才会停止。

从上面的演示动图可以看出来深搜的效率并不高,而且深搜是盲目的一条条路去测试能否到达终点,时间复杂度较高,不过如果多次运行程序,也会发现,有时候少数几次试探就能到达终点,这也就导致了有时候会出现深搜比较快的情况,不过毕竟是少数情况,总的来说深搜还是较慢的。

END

在最后Last but not least

本文介绍了深度优先搜索求解迷宫出路,通过讲解二叉树的先序遍历非递归程序引入迷宫的非递归深搜程序,两者有相似之处也有不同之处,二叉树一个结点最多两个分叉,而迷宫中一个点却有四个方向,但两者都可以使用栈来实现非递归程序。

思路很简单,感兴趣的读者可以自行尝试,在下一篇推文中,将继续介绍第二种求解迷宫出路的方法——广度优先搜索。

往期 精彩回顾

Python数据结构——树(五)

数据结构课程设计——迷宫求解(一)

数据结构课程设计——迷宫求解(二)

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值