【数据结构】10分钟教你用栈求解迷宫老鼠问题超详细教程附C++源代码

问题描述

给定一张迷宫地图和一个迷宫入口,然后进入迷宫探索找到一个出口。如下图所示:

分享图片

该图是一个矩形区域,有一个入口和出口。迷宫内部包含不能穿越的墙壁或者障碍物。这些障碍物沿着行和列放置,与迷宫的边界平行。迷宫的入口在左上角,出口在右下角。

问题分析

  1. 首先要有一张迷宫地图,地图由两部分组成:

    (1)一是迷宫中各处的位置坐标,

    (2)二是迷宫各位置处的状态信息,即该处是墙还是路

所以,该迷宫地图可由一个二维数组来表示。数组的横纵坐标表示迷宫各处的位置坐标,数组元素表示各位置处的状态信息。

2.在这里,假定:

(1)迷宫地图是m*n的,即二维数组是m行n列的。
(2)在迷宫中用1表示墙,用0表示路。当然,为了便于标识,我们后面还会用其他数字代表更多含义。

因此,迷宫的地图一个刻画如下:

分享图片

现在我们要找一条从入口到出口的路径。路径是一个由位置组成的序列,每一个位置都没有障碍,并且除入口外,路径上的每一个位置都是前一个位置在东西南北方向上相邻的一个位置。

不过,考虑到边界问题不太好处理。我们做这样的工作,在地图外围加一层围墙,给它全部填上1。这样,在处理各个坐标时,都没有差别了。这样一来便大大简化了我们的工作。

分享图片

下面我们要编写程序求解这个问题。

程序设计

这次还是采用一个简单的模块化来设计这个程序。那么主要有下面几个模块:

  1. 显示欢迎信息
  2. 初始化工作
  3. 生成地图
  4. 找路
  5. 打印地图和路径

下面我们分别完成这些功能。

显示欢迎信息

这个模块就很简单了,输出一些信息提醒使用者就行,主要是为了增加程序的友好性而设置的。大家根据自己的需要自行发挥。例如我的就很随便了:

1void welcome()
2
{
3    cout << "welcome to RAT IN MAZE" << endl;
4    system("pause");
5    system("cls");
6}

分享图片

初始化工作

这个主要是设置一些全局变量的取值和完成内存的分配,地图的存储还是从堆上分配内存比较好。因为一般来说,考虑到地图可能会很大,这样需要的存储空间就很多了。在这里一并把相关的全局变量给讲解了吧。

 1typedef struct  
2{

3    int row;
4    int col;
5}POSITION;
6
7
8const POSITION maze_size = { 20 , 60 };
9
10int ** const maze = new int*[maze_size.row + 2];
11
12stack<POSITION> path;
13POSITION offset[4];//direction
  • POSITION结构体
    坐标结构体变量类型,很容易理解,有两个成员变量:x坐标和y坐标。

  • maze_size
    定义地图的大小,实际分配内存的时候,我们还需要考虑地图边界也需要存储空间。总之,我们的地图坐标范围是1 to maze_size。

  • maze
    二位数组,存储地图,分配的时候+2是用来存储边界的。至于const则是约束指针不改变。不过我们的地图数组是根据maze_size大小动态分配的。

  • path
    用来存路径的。

  • offset
    用来设置位置偏移的。比如我们当前位置是(row = 1,col = 1),那么通过row + 1便可往下走,row - 1就是往上走。col + 1往右走,col - 1 往左走。等等。通过坐标加减offset偏移,便可以移动了。

 1void init()
2
{
3    //偏移
4    offset[0].row = 0; offset[0].col = 1//right
5    offset[1].row = 1; offset[1].col = 0//down
6    offset[2].row = 0; offset[2].col = -1//left
7    offset[3].row = -1; offset[3].col = 0//up
8
9    //maze = new int*[maze_size.row + 2];
10    for (int i = 0; i < maze_size.row + 2; i++)
11    {
12        maze[i] = new int[maze_size.col + 2];
13    }
14}

这个代码就是设置偏移的数值,以及动态分配地图数组了。

生成地图

生成地图还是根据地图尺寸,然后随机设置障碍。不过要注意障碍出现的概率设置得小一点,不然地图一般无解。可以用rand()随机数来做。这一步也要把围墙设置好。

 1//地图范围1 - maze_size 有围墙
2void randomMaze()
3
{
4    int i, j, rate;
5
6    for (i = 0; i < maze_size.row + 2; i++)
7    {
8        for (j = 0; j < maze_size.col + 2; j++)
9        {
10            //设置围墙
11            if ((i == 0) || (i == maze_size.row + 1) || (j == 0) || (j == maze_size.col + 1))
12            {
13                maze[i][j] = 1;
14            }
15            else
16            {
17                rate = rand() % 10+1;
18                if (rate <= 3)
19                {
20                    maze[i][j] = 1;//随机生成障碍
21                }
22                else
23                {
24                    maze[i][j] = 0;
25                }
26            }
27        }
28    }
29    //最后保证起点和终点能走
30    maze[1][1] = maze[maze_size.row][maze_size.col] = 0;
31}

找路

这个是整个程序设计的核心功能,没有之一。在写代码之前,我们需要弄明白,到底怎么找路呢?

  1. 首先,把迷宫入口作为当前位置。
  2. 如果当前位置是迷宫出口,那么已经找到一条路径了,程序结束。
  3. 如果当前位置不是出口,则在当前位置放置障碍物,表示这里已经来过,防止下次又重复绕回来。然后检查相邻位置是否能走。
  4. 如果一个相邻位置能走,就移动到这个位置上。然后在新的位置上重新开始寻找出口。如果不能走,就尝试下一个相邻位置。
  5. 如果所有的相邻位置都不能走了,则回退到上一个位置,重新选择上一个位置的其他相邻位置,继续探索。
  6. 如果所有的相邻位置都被探索过了,仍然找不到路径,那说明这个迷宫不存在这样的路径。

例如,下面的地图:

图8-9

                                                                                                                                        图8-9

分享图片

好了,说了这么多,相信大家已经了解得差不多,并且跃跃欲试了。

 1bool findPath()
2
{
3    POSITION here; //当前位置
4    here.row = here.col = 1;
5    maze[1][1] = 3//放置障碍,防止回来
6    int option = 0//next step
7    const int lastOption = 3
8
9    //find a path
10    while ( here.row != maze_size.row || here.col != maze_size.col)
11    {
12        //not reach the end
13        int r, c;
14        while (option <= lastOption)
15        {
16            r = here.row + offset[option].row;
17            c = here.col + offset[option].col;
18            if (maze[r][c] == 0)
19            {
20                break;
21            }
22            option++;//next choice
23        }
24
25        //相邻的位置能走?
26        if (option <= lastOption)
27        {
28            path.push(here);
29            here.row = r;
30            here.col = c;
31            maze[r][c] = 3//走过了
32            option = 0;
33        }
34        else
35        {
36            if (path.empty())
37            {
38                return false;
39            }
40            //go back
41            maze[here.row][here.col] = 3//此路不可通
42            here = path.top();
43            path.pop();
44            option = 0;
45        }
46    }
47
48    maze[maze_size.row][maze_size.col] = 2;
49
50    return true;
51}

相关代码如上面所示,结合前面的讲解,相信大家也能看懂。

打印地图和路径

这个功能就比较简单了,主要是根据maze的信息,生成相应的地图显示出来给大家直观的看到。对于maze里面存的数值,我们也可以作一个小小的规定:

  • 0
    表示位置可通行。
  • 1
    表示位置有障碍。
  • 2
    表示该位置处在找到的路径上面。
  • 3
    探索过程中放置的障碍物。这个障碍物和1表示的障碍物不同的是,这个障碍我们放置的,和生成地图时的固定障碍物不同。因此还是要区分开来的。

然后打印的时候,遍历maze数组,遇到:

  • 0
    打印空格。
  • 1
    打印*号。
  • 2
    打印#号,并且设置下颜色。
  • 3
    还是打印空格。(或者根据自己的喜好打印另外的符号,这样就可以把探索过的所有位置显示出来。)

最后在打印最终的地图和路径之前,如果找到一条路径。我们还要根据path中的路径,在maze里面设置好相应的值,才做最终的输出:

 1void setPathOnMaze()
2
{
3    POSITION pos;
4    while (!path.empty())
5    {
6        pos = path.top();
7        path.pop();
8        maze[pos.row][pos.col] = 2;//路径
9    }
10}
11
12然后,可以开始输出我们的地图了。

具体代码:

 1void outputMaze()
2
{
3    int i, j;
4    for (i = 0; i < maze_size.row + 2; i++)
5    {
6        for (j = 0; j < maze_size.col + 2; j++)
7        {
8            if (maze[i][j] == 1)
9            {
10                cout << "*";
11            }
12            else if ((maze[i][j] == 0) || (maze[i][j] == 3))
13            {
14                cout << " ";
15            }
16            else
17            {
18                HANDLE hOut;
19                hOut = GetStdHandle(STD_OUTPUT_HANDLE);
20                SetConsoleTextAttribute(hOut,FOREGROUND_GREEN | FOREGROUND_INTENSITY);
21                cout << "#";
22                SetConsoleTextAttribute(hOut, FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE);
23            }
24        }
25        cout << endl;
26    }
27}

最终效果

  1. 没有找到路的情况:

    分享图片

  2. 找到了路径:

    分享图片

代码获取

欲获取代码,请关注我们的微信公众号【程序猿声】,在后台回复:rat。即可下载。

微信公众号

推荐文章:10分钟教你用Python做个打飞机小游戏超详细教程

推荐文章:10分钟教你用python下载和拼接微信好友头像图片

推荐文章:10分钟教你用python一行代码搞点大新闻

推荐文章:10分钟教你用python打造贪吃蛇超详细教程

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


【啊哈!算法】算法3:最常用的排序——快速排序       上一节的冒泡排序可以说是我们学习第一个真正的排序算法,并且解决了桶排序浪费空间的问题,但在算法的执行效率上却牺牲了很多,它的时间复杂度达到了O(N2)。假如我们的计算机每秒钟可以运行10亿次,那么对1亿个数进行排序,桶排序则只需要0.1秒,而冒泡排序则需要1千万秒,达到115天之久,是不是很吓人。那有没有既不浪费空间又可以快一点的排序算法
匿名组 这里可能用到几个不同的分组构造。通过括号内围绕的正则表达式就可以组成第一个构造。正如稍后要介绍的一样,既然也可以命名组,大家就有考虑把这个构造作为匿名组。作为一个实例,请看看下列字符串: “08/14/57 46 02/25/59 45 06/05/85 18 03/12/88 16 09/09/90 13“ 这个字符串就是由生日和年龄组成的。如果需要匹配年两而不要生日,就可以把正则
选择排序:从数组的起始位置处开始,把第一个元素与数组中其他元素进行比较。然后,将最小的元素方式在第0个位置上,接着再从第1个位置开始再次进行排序操作。这种操作一直到除最后一个元素外的每一个元素都作为新循环的起始点操作过后才终止。 public void SelectionSort() { int min, temp;
public struct Pqitem { public int priority; public string name; } class CQueue { private ArrayList pqueue; public CQueue() { pqueue
在编写正则表达式的时候,经常会向要向正则表达式添加数量型数据,诸如”精确匹配两次”或者”匹配一次或多次”。利用数量词就可以把这些数据添加到正则表达式里面了。 数量词(+):这个数量词说明正则表达式应该匹配一个或多个紧紧接其前的字符。 string[] words = new string[] { "bad", "boy", "baad", "baaad" ,"bear", "b
来自:http://blog.csdn.net/morewindows/article/details/6678165/归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。首先考虑下如何将将二个有序数列合并。这个非常简单,只要从比较二个数列的第一个数,谁小就先取谁,取了后就在对应数列中删除这个数。然后再进行比较,如果有数列
插入排序算法有两层循环。外层循环会啄个遍历数组元素,而内存循环则会把外层循环所选择的元素与该元素在数组内的下一个元素进行比较。如果外层循环选择的元素小于内存循环选择的元素,那么瘦元素都想右移动以便为内存循环元素留出位置。 public void InsertionSort() { int inner, temp;
public int binSearch(int value) { int upperBround, lowerBound, mid; upperBround = arr.Length - 1; lowerBound = 0; while (lowerBound <= upper
虽然从表内第一个节点到最后一个节点的遍历操作是非常简单的,但是反向遍历链表却不是一件容易的事情。如果为Node类添加一个字段来存储指向前一个节点的连接,那么久会使得这个反向操作过程变得容易许多。当向链表插入节点的时候,为了吧数据复制给新的字段会需要执行更多的操作,但是当腰吧节点从表移除的时候就能看到他的改进效果了。 首先需要修改Node类来为累增加一个额外的链接。为了区别两个连接,这个把指
八、树(Tree)树,顾名思义,长得像一棵树,不过通常我们画成一棵倒过来的树,根在上,叶在下。不说那么多了,图一看就懂:当然了,引入了树之后,就不得不引入树的一些概念,这些概念我照样尽量用图,谁会记那么多文字?树这种结构还可以表示成下面这种方式,可见树用来描述包含关系是很不错的,但这种包含关系不得出现交叉重叠区域,否则就不能用树描述了,看图:面试的时候我们经常被考到的是一种叫“二叉树”的结构,二叉
Queue的实现: 就像Stack类的实现所做的一样,Queue类的实现用ArrayList简直是毋庸置疑的。对于这些数据结构类型而言,由于他们都是动态内置的结构,所以ArrayList是极好的实现选择。当需要往队列中插入数据项时,ArrayList会在表中把每一个保留的数据项向前移动一个元素。 class CQueue { private ArrayLis
来自:http://yingyingol.iteye.com/blog/13348911 快速排序介绍:快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要Ο(n log n)次比较。在最坏状况下则需要Ο(n2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地
Stack的实现必须采用一种基本结构来保存数据。因为再新数据项进栈的时候不需要担心调整表的大小,所以选择用arrayList.using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks;using System.Collecti
数组类测试环境与排序算法using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks;namespace Data_structure_and_algorithm{ class CArray { pr
一、构造二叉树 二叉树查找树由节点组成,所以需要有个Node类,这个类类似于链表实现中用到的Node类。首先一起来看看Node类的代码。 public class Node { public int Data; public Node Left; public Node Right; public v
二叉树是一种特殊的树。二叉树的特点是每个结点最多有两个儿子,左边的叫做左儿子,右边的叫做右儿子,或者说每个结点最多有两棵子树。更加严格的递归定义是:二叉树要么为空,要么由根结点、左子树和右子树组成,而左子树和右子树分别是一棵二叉树。 下面这棵树就是一棵二叉树。         二叉树的使用范围最广,一棵多叉树也可以转化为二叉树,因此我们将着重讲解二叉树。二叉树中还有连两种特殊的二叉树叫做满二叉树和
上一节中我们学习了队列,它是一种先进先出的数据结构。还有一种是后进先出的数据结构它叫做栈。栈限定只能在一端进行插入和删除操作。比如说有一个小桶,小桶的直径只能放一个小球,我们现在向小桶内依次放入2号、1号、3号小球。假如你现在需要拿出2号小球,那就必须先将3号小球拿出,再拿出1号小球,最后才能将2号小球拿出来。在刚才取小球的过程中,我们最先放进去的小球最后才能拿出来,而最后放进去的小球却可以最先拿
msdn中的描述如下:(?= 子表达式)(零宽度正预测先行断言。) 仅当子表达式在此位置的右侧匹配时才继续匹配。例如,w+(?=d) 与后跟数字的单词匹配,而不与该数字匹配。此构造不会回溯。(?(零宽度正回顾后发断言。) 仅当子表达式在此位置的左侧匹配时才继续匹配。例如,(?此构造不会回溯。msdn描述的比较清楚,如:w+(?=ing) 可以匹配以ing结尾的单词(匹配结果不包括ing),(
1.引入线索二叉树 二叉树的遍历实质上是对一个非线性结构实现线性化的过程,使每一个节点(除第一个和最后一个外)在这些线性序列中有且仅有一个直接前驱和直接后继。但在二叉链表存储结构中,只能找到一个节点的左、右孩子信息,而不能直接得到节点在任一遍历序列中的前驱和后继信息。这些信息只有在遍历的动态过程中才能得到,因此,引入线索二叉树来保存这些从动态过程中得到的信息。 2.建立线索二叉树 为了保
排序与我们日常生活中息息相关,比如,我们要从电话簿中找到某个联系人首先会按照姓氏排序、买火车票会按照出发时间或者时长排序、买东西会按照销量或者好评度排序、查找文件会按照修改时间排序等等。在计算机程序设计中,排序和查找也是最基本的算法,很多其他的算法都是以排序算法为基础,在一般的数据处理或分析中,通常第一步就是进行排序,比如说二分查找,首先要对数据进行排序。在Donald Knuth 的计算机程序设