递归、回溯、队列三种方式求解迷宫问题

        迷宫问题是机器智能中一种常见的问题,我们在生活中也会常常遇到这类问题:我们会顺着某一方向向前探索,如果遇到岔口,则要选择某一个路口前进,会出现两种可能性,若能走通,则继续往前走,最后顺利通到出口处;否则沿原路退回,换一个方向在继续探索,直至所有可能的通路都探索到为止。

        用问题定义可以描述为迷宫是一个M*N的二维矩阵,其中0为墙,1为路,入口在第一列,出口在最后一列。要求从入口开始,从出口结束,按照上,下,左,右的顺序来搜索路径,设计程序,对任意设定的迷宫,求出从入口到出口的所有通路,如图所示。



求解方法



  1. 递归求解


1.1递归

        程序调用自身的编程技巧称为递归。递归做为一种算法程序设计语言中广泛应用。一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的能力在于用有限的语句来定义对象的无限集合。一般来说,递归需要有边界条件、递归前进段和递归返回段。当边界条件不满足时,递归前进;当边界条件满足时,递归返回。


1.2分析过程

1.2.1基本思路

        ①每个时刻总有一个当前位置,开始时这个位置是迷宫入口。

        ②如果当前位置就是出口,问题已解决。

        ③否则,如果从当前位置己无路可走,当前的探查失败,回退一步。

        ④取一个可行相邻位置用同样方式探查,如果从那里可以找到通往出口的路径,那么从当前位置到出口的路径也就找到了。

1.2.2算法过程

        在整个计算开始时,把迷宫的入口作为检查的当前位置,算法过程就是:

        ①用mark函数确定当前位置。

        ②检查当前位置是否为出口,如果是则成功结束。

        ③逐个检查当前位置的四邻是否可以通达出口,用递归调用自身的函数。

        ④如果对四邻的探索都失败,报告失败。


1.3代码和结果

1.3.1代码

dirs=[(0,1),(1,0),(0,-1),(-1,0)] #当前位置四个方向的偏移量

path=[]              #存找到的路径

def mark(maze,pos):  #给迷宫maze的位置pos标"2"表示“倒过了”

    maze[pos[0]][pos[1]]=2

def passable(maze,pos): #检查迷宫maze的位置pos是否可通行

    return maze[pos[0]][pos[1]]==0

def find_path(maze,pos,end):

    mark(maze,pos)

    if pos==end:

        print(pos,end=" ")  #已到达出口,输出这个位置。成功结束

        path.append(pos)

        return True

    for i in range(4):      #否则按四个方向顺序检查

        nextp=pos[0]+dirs[i][0],pos[1]+dirs[i][1]

        #考虑下一个可能方向

        if passable(maze,nextp):        #不可行的相邻位置不管

            if find_path(maze,nextp,end):#如果从nextp可达出口,输出这个位置,成功结束

                print(pos,end=" ")

                path.append(pos)

                return True

    return False

def see_path(maze,path):     #使寻找到的路径可视化

    for i,p in enumerate(path):

        if i==0:

            maze[p[0]][p[1]] ="E"

        elif i==len(path)-1:

            maze[p[0]][p[1]]="S"

        else:

            maze[p[0]][p[1]] =3

    print("\n")

    for r in maze:

        for c in r:

            if c==3:

                print('\033[0;31m'+"*"+" "+'\033[0m',end="")

            elif c=="S" or c=="E":

                print('\033[0;34m'+c+" " + '\033[0m', end="")

            elif c==2:

                print('\033[0;32m'+"#"+" "+'\033[0m',end="")

            elif c==1:

                print('\033[0;;40m'+" "*2+'\033[0m',end="")

            else:

                print(" "*2,end="")

        print()

if __name__ == '__main__':

    maze=[[1,1,1,1,1,1,1,1,1,1,1,1,1,1],

          [1,0,0,0,1,1,0,0,0,1,0,0,0,1],

          [1,0,1,0,0,0,0,1,0,1,0,1,0,1],

          [1,0,1,0,1,1,1,1,0,1,0,1,0,1],

          [1,0,1,0,0,0,0,0,0,1,1,1,0,1],

          [1,0,1,1,1,1,1,1,1,1,0,0,0,1],

          [1,0,1,0,0,0,0,0,0,0,0,1,0,1],

          [1,0,0,0,1,1,1,0,1,0,1,1,0,1],

          [1,0,1,0,1,0,1,0,1,0,1,0,0,1],

          [1,0,1,0,1,0,1,0,1,1,1,1,0,1],

          [1,0,1,0,0,0,1,0,0,1,0,0,0,1],

          [1,1,1,1,1,1,1,1,1,1,1,1,1,1]]

    start=(1,1)

    end=(10,12)

    find_path(maze,start,end)

    see_path(maze,path)

1.3.2结果

        运行结果如图所示,在上面给出的迷宫里,找到了15个点,这15个点的坐标就是递归求解方法最后找出的路线。在n*n的迷宫里,因为需要遍历所有的点,时间复杂度就是O(n*n)。


递归结果



  1. 回溯求解


2.1回溯

        回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。


2.2分析过程

2.2.1基本思路

        迷宫问题主要思路就是从入口开始,顺着路(1)行走,遇墙(0)改变路线,有四个方向,向上向下向左向右,每进入一格都要遍历这4种状态。因此可以根据子集树模板进行,如图所示。这次用到的方法是回溯,用该方法对解空间进行搜索。


子集树

2.2.2算法过程

if 到达了目的地:

      打印结果矩阵

else:

      if 此位置已经走过了:

            返回上一步

      else:

            尝试向四周走,递归此核心思路


2.3代码和结果

2.3.1代码

class Maze():

    # 初始化迷宫的规格大小

    def __init__(self, n):

        self.n = n

        # 初始化结果矩阵

        self.flag = [[0 for _ in range(n)] for _ in range(n)]

    # 判断位置是否合法,如果可以走,那么返回True,否则返回False

    def isSafe(self, maze, x, y):

        return x >= 0 and x <= self.n - 1 and y >= 0 and y <= self.n - 1 and maze[x][y] == 1

    # 打印矩阵

    def printSolution(self, flag):

        for i in flag:

            for j in i:

                print('%4s' % str(j), end='')

            print('\n')

    # 获取走出迷宫的结果矩阵

    def getPath(self, maze, x, y):

        # 如果此处位置合法但是此处走过了,返回到上一步

        if self.isSafe(maze, x, y) and self.flag[x][y] == '-':

            return False

        # 如果到达了目的地,返回True

        if x == self.n - 1 and y == self.n - 1:

            self.flag[x][y] = '-'

            self.printSolution(self.flag)

            return True

        # 没有到达目的地,继续往下走

        if self.isSafe(maze, x, y):

            # 标记结果矩阵中当前位置为-

            self.flag[x][y] = '-'

            # 尝试向右走,递归

            if self.getPath(maze, x + 1, y):

                return True

            # 尝试向下走,递归

            if self.getPath(maze, x, y + 1):

                return True

            # 尝试向左走,递归

            if self.getPath(maze, x - 1, y):

                return True

            # 尝试向上走,递归

            if self.getPath(maze, x, y - 1):

                return True

            # 此路不通,标记当前的单元为0,表示此处不可能到达目的地,返回上一步

            self.flag[x][y] = 0

            return False

        # 此位置不合法,返回False

        return False

if __name__ == '__main__':

    maze = [[1, 1, 0, 1, 1, 1, 1, 1, 0],

            [0, 1, 0, 1, 0, 1, 0, 1, 1],

            [0, 1, 1, 1, 0, 1, 0, 0, 0],

            [0, 1, 0, 0, 1, 1, 0, 0, 0],

            [0, 1, 1, 1, 1, 0, 1, 1, 1],

            [0, 0, 1, 0, 0, 0, 1, 0, 1],

            [1, 1, 1, 1, 1, 1, 1, 0, 1],

            [0, 1, 1, 0, 0, 0, 0, 0, 1],

            [1, 0, 1, 0, 0, 0, 0, 0, 1], ]

    rat = Maze(len(maze))

    ret = rat.getPath(maze, 0, 0)

    if ret == False:

        print("No solution!")

2.3.2结果

        用回溯法求解迷宫问题运行结果如图所示,其中“-”表示的就是通路,用回溯法求解迷宫问题还是需要遍历到每一个位置,因此时间复杂度还是O(n*n)。


回溯结果



  1. 队列求解

3.1队列

        队列是一种特殊的线性表,特殊之处在于它只允许在表的前端进行删除操作,而在表的后端进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。队列中没有元素时,称为空队列。

        队列的数据元素又称为队列元素。在队列中插入一个队列元素称为入队,从队列中删除一个队列元素称为出队。因为队列只允许在一端插入,在另一端删除,所以只有最早进入队列的元素才能最先从队列中删除,故队列又称为先进先出线性表


3.2分析过程

3.2.1基本思路

        队列求解算法中,以队列存储可以探索的位置。利用队列先进先出的特点,实现在每个分支上同时进行搜索路径,直到找到出口。这是一种广度优先搜索的方法。

3.2.2算法思路

        首先构建一个空队列,手动输入一个迷宫,进行下一步,搜索通路,有通路直至到达终点,无通路就退回到起点。搜索过程中,遇到前面不再有出口的时候,就要使当前队尾结点出队,于是就要对当前队列从队头到倒数第二个结点依次转移,释放出队尾结点。


3.3代码和结果

3.3.1代码

from collections import deque

maze = [

     [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],

     [1, 0, 0, 1, 0, 0, 0, 1, 0, 1],

     [1, 0, 0, 1, 0, 0, 0, 1, 0, 1],

     [1, 0, 0, 0, 0, 1, 1, 0, 0, 1],

     [1, 0, 1, 1, 1, 0, 0, 0, 0, 1],

     [1, 0, 0, 0, 1, 0, 0, 0, 0, 1],

     [1, 0, 1, 0, 0, 0, 1, 0, 0, 1],

     [1, 0, 1, 1, 1, 0, 1, 1, 0, 1],

     [1, 1, 0, 0, 0, 0, 0, 0, 0, 1],

     [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]

 ]

dirs = [

     lambda x,y:(x+1,y),#下

     lambda x,y:(x,y-1),#左

     lambda x,y:(x,y+1),#右

     lambda x,y:(x-1,y),#上

 ]

def print_r(path):

     curNode = path[-1]

     realpath = []

     while curNode[2] != -1:

         realpath.append(curNode[0:2])

         curNode = path[curNode[2]]

     realpath.append(curNode[0:2]) # 把起点放进去

     realpath.reverse()

     print(len(realpath))

     for i in realpath:

         print(i)

def maze_path_queue(x1,y1,x2,y2):

     maze[x1][y1]=2

     queue = deque()

     queue.append((x1,y1,-1))

     path = []

     while len(queue)>0:

         curNode = queue.popleft()

         path.append(curNode)

         if curNode[0] == x2 and curNode[1] == y2:

             print_r(path)

             return True

         for dir in dirs:

             nextNode = dir(curNode[0],curNode[1])

             if maze[nextNode[0]][nextNode[1]]==0:

                 # 后续节点进队,记录哪个节点带他来的

                 node = (nextNode[0],nextNode[1],len(path)-1)

                 queue.append(node)

                 maze[nextNode[0]][nextNode[1]] = 2  # 标记为已经走过

     else:

         print('没有路')

         return False

maze_path_queue(1,1,8,8)

3.3.2结果

如图所示,这是队列求解最后的结果,需要遍历每个位置,时间复杂度为O(n*n)。


队列求解

  • 4
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Data Structures, Algorithms, and Applications in C++, Second Edition 出版者的话 译者序 前言 第一部分 预备知识 第1章 C++回顾 1.1 引言 1.2 函数与参数 1.2.1 传值参数 1.2.2 模板函数 1.2.3 引用参数 1.2.4 常量引用参数 1.2.5 返回值 1.2.6 重载函数 1.3 异常 1.3.1 抛出异常 1.3.2 处理异常 1.4 动态存储空间分配 1.4.1 操作符new 1.4.2 一维数组 1.4.3 异常处理 1.4.4 操作符delete 1.4.5 二维数组 1.5 自有数据类型 1.5.1 类currency 1.5.2 一种不同的描述方法 1.5.3 操作符重载 1.5.4 友元和保护性类成员 1.5.5 增加#ifndef、#define和#endif语句 1.6 异常类illegalParameterValue 1.7 递归函数 1.7.1 递归的数学函数 1.7.2 归纳 1.7.3 C++递归函数 1.8 标准模板库 1.9 测试与调试 1.9.1 什么是测试 1.9.2 测试数据的设计 1.9.3 调试 1.10 参考及推荐读物 第2章 程序性能分析 2.1 什么是程序性能 2.2 空间复杂度 2.2.1 空间复杂度的组成 2.2.2 举例 2.3 时间复杂度 2.3.1 时间复杂度的组成 2.3.2 操作计数 2.3.3 最好、最坏和平均操作计数 2.3.4 步数 第3章 渐近记法 3.1 引言 3.2 渐近记法 3.2.1 大Ο记法 3.2.2 渐近记法Ω和Θ 3.3 渐近数学(可选) 3.3.1 大O记法 3.3.2 Ω记法 3.3.3 Θ记法 3.3.4 小ο记法 3.3.5 特性 3.4 复杂度分析举例 3.5 实际复杂度 3.6 参考及推荐读物 第4章 性能测量 4.1 引言 4.2 选择实例的大小 4.3 设计测试数据 4.4 实验设计 4.5 高速缓存 4.5.1 简单计算机模型 4.5.2 缓存未命中对运行时间的影响 4.5.3 矩阵乘法 4.6 参考及推荐读物 第二部分 数据结构 第5章 线性表——数组描述 5.1 数据对象和数据结构 5.2 线性表数据结构 5.2.1 抽象数据类型linearList 5.2.2 抽象类linearList 5.3 数组描述 5.3.1 描述 5.3.2 变长一维数组 5.3.3 类arrayList 5.3.4 C++迭代器 5.3.5 arrayList的一个迭代器 5.4 vector的描述 5.5 在一个数组中实现的多重表 5.6 性能测量 5.7 参考及推荐读物 第6章 线性表——链式描述 6.1 单向链表 6.1.1 描述 6.1.2 结构chainNode 6.1.3 类chain 6.1.4 抽象数据类型linearList的扩充 6.1.5 类extendedChain 6.1.6 性能测量 6.2 循环链表和头节点 6.3 双向链表 6.4 链表用到的词汇表 6.5 应用 6.5.1 箱子排序 6.5.2 基数排序 6.5.3 凸包 6.5.4 并查集 第7章 数组和矩阵 7.1 数组 7.1.1 抽象数据类型 7.1.2 C++数组的索引 7.1.3 行主映射和列主映射 7.1.4 用数组的数组来描述 7.1.5 行主描述和列主描述 7.1.6 不规则二维数组 7.2 矩阵 7.2.1 定义和操作 7.2.2 类matrix 7.3 特殊矩阵 7.3.1 定义和应用 7.3.2 对角矩阵 7.3.3 三对角矩阵 7.3.4 三角矩阵 7.3.5 对称矩阵 7.4 稀疏矩阵 7.4.1 基本概念 7.4.2 用单个线性表描述 7.4.3 用多个线性表描述 7.4.4 性能测量 第8章 栈 8.1 定义和应用 8.2 抽象数据类型 8.3 数组描述 8.3.1 作为一个派生类实现 8.3.2 类arrayStack 8.3.3 性能测量 8.4 链表描述 8.4.1 类derivedLinkedStack 8.4.2 类linkedStack 8.4.3 性能测量 8.5 应用 8.5.1 括号匹配 8.5.2 汉诺塔 8.5.3 列车车厢重排 8.5.4 开关盒布线 8.5.5 离线等价类问题 8.5.6 迷宫老鼠 8.6 参考及推荐读物 第9章 队列 9.1 定义和应用 9.2 抽象数据类型 9.3 数组描述 9.3.1 描述 9.3.2 类arrayQueue 9.4 链表描述 9.5 应用 9.5.1 列车车厢重排 9.5.2 电路布线 9.5.3 图元识别 9.5.4 工厂仿真 9.6 参考及推荐读物 第10章
由于没有给出具体的迷宫问题,下面分别介绍三种解法的基本思路和实现方式。 1. 采用栈求解迷宫问题 栈是一种后进先出的数据结构,我们可以利用栈来记录迷宫中走过的路径。具体实现方式如下: 1)定义一个栈,用来记录当前位置的坐标(x, y)以及走到该位置的方向(0表示未走过,1表示向上,2表示向右,3表示向下,4表示向左)。 2)从起点开始,将其入栈,并标记已走过。 3)每次从栈顶取出一个位置,判断其是否为终点,若是,则输出路径并结束程序;否则,依次判断该位置四周的位置是否可走,若可走,则将其入栈,并标记已走过。 4)如果四周都走不通,则出栈,回溯到上一个位置,继续搜索。 2. 采用递归算法求解迷宫问题 递归是一种自我调用的算法,我们可以将迷宫问题转化为一个递归问题,具体实现方式如下: 1)定义一个函数,用来表示从当前位置出发是否能到达终点。 2)在函数中,先判断当前位置是否为终点,若是,则返回true。 3)否则,依次判断该位置四周的位置是否可走,若可走,则递归调用函数判断该位置是否能到达终点。 4)如果四周都走不通,则返回false。 5)在主函数中,从起点开始调用递归函数,若返回true,则输出路径,否则输出无解。 3. 采用队列求解迷宫问题 队列是一种先进先出的数据结构,我们可以利用队列来记录迷宫中走过的路径。具体实现方式如下: 1)定义一个队列,用来记录当前位置的坐标(x, y)以及走到该位置的方向(0表示未走过,1表示向上,2表示向右,3表示向下,4表示向左)。 2)从起点开始,将其入队,并标记已走过。 3)每次从队头取出一个位置,判断其是否为终点,若是,则输出路径并结束程序;否则,依次判断该位置四周的位置是否可走,若可走,则将其入队,并标记已走过。 4)如果四周都走不通,则出队,继续搜索队列中下一个位置。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值