探索迷宫
探讨一个与蓬勃发展的机器人领域相关的问题:走出迷宫。如果你有一个Roomba扫地机器人,或许
能利用学到的知识对它进行重新编程。我们要解决的问题是帮助小乌龟走出虚拟的迷宫。迷宫问题源自忒修
斯大战牛头怪的古希腊神话传说。相传,在迷宫里杀死牛头怪之后,忒修斯用一个线团找到了迷宫的出口。
假设小乌龟被放置在迷宫里的某个位置,我们要做的是帮助它爬出迷宫,如图所示
为简单起见,假设迷宫被分成许多格,每一格要么是空的,要么被墙堵上。小乌龟只能沿着空的
格子爬行,如果遇墙,就必须转变方向。它需要如下的系统化过程来找到出路。
(1)从起始位置开始,寿险向北移动一格,然后在新的位置再递归地重复本过程。
(2)如果第一步往北行不通,就尝试向南移动一格,然后在递归重复本过程。
(3)如果向南也行不通,就尝试向西移动一格,然后递归地重复本过程。
(4)如果向北、向南、向西都不行,就尝试向东移动一格,然后递归地重复本过程。
(5)如果四个方向都不行,就意味着没有出路。
整个过程看上去非常简单,但是有许多细节需要讨论。假设递归过程的第一步是向北移动一格。
根据上述过程,下一步也是向北移动一格。但是,如果北面有墙,必须根据递归过程的第二步向南移
动一格。不行的是向南移动一格之后回到起点。如果继续执行该递归过程,就会又向北移动一格,然
后又退回来,从而陷入无限循环中。所以,必须通过一格策略来记住到过的地方,本例假设小乌龟一
边爬,一边丢面包屑。如果往某个方向走一格之后发现有面包屑,就知道应该退回去,然后尝试递归
过程的下一步。查看这个算法的代码时会发现,退回去就是从递归函数调用中返回。
和考察其他递归算法时一样,让我们来看看上述算法的基本情况,其中一些可以根据之前的描述
猜到。这个算法需要考虑以下4种基本情况。
(1)小乌龟遇到墙。由于格子被墙堵上,因此无法再继续探索。
(2)小乌龟遇到了已经走过的格子。在这种情况下,我们不希望它继续探索,不然会陷入循环。
(3)小乌龟找到了出口。
(4)四个方向都行不通。
为了使程序运行起来,需要通过一种方式标识迷宫。我们使用turtle模块来绘制和探索迷宫,以增
加趣味性。迷宫对象提供下列方法,可用编写搜索算法。
--init--读入一个代表迷宫数据文件,初始化迷宫的内部表示,并且找到小乌龟的起始位置。
drawMaze在屏幕上的一个窗口中绘制迷宫。
updatePosition更新迷宫的内部表示,并且修改小乌龟在迷宫中的位置。
isExit检查小乌龟的当前位置是否为迷宫的出口。
除此之外,Maze类还重载了索引运算符[ ],以便算法访问任一格的状态。
以下代买清单展示了搜索函数searchFrom的代码。该函数接受3个参数:迷宫对象、起始行,以及
起始列。由于该函数的每一次递归调用在逻辑上都是重新开始搜索的,因此定义接受3个参数非常重要。
def searchFrom(maze, startRow, startColumn): # 从初始位置开始尝试四个方向,直到找到出路。 # 1. 遇到障碍 if maze[startRow][startColumn] == OBSTACLE: return False # 2. 发现已经探索过的路径或死胡同 if maze[startRow][startColumn] == TRIED or maze[startRow][startColumn]== DEAD_END: return False # 3. 发现出口 if maze.isExit(startRow, startColumn): maze.updatePosition(startRow, startColumn, PART_OF_PATH)#显示出口位置,注释则不显示此点 return True maze.updatePosition(startRow, startColumn, TRIED)#更新迷宫状态、设置海龟初始位置并开始尝试 # 4. 依次尝试每个方向 found = searchFrom(maze, startRow - 1, startColumn) or searchFrom(maze, startRow + 1, startColumn) or searchFrom(maze, startRow, startColumn - 1) or searchFrom(maze, startRow, startColumn + 1) if found: #找到出口 maze.updatePosition(startRow, startColumn, PART_OF_PATH)#返回其中一条正确路径 else: #4个方向均是死胡同 maze.updatePosition(startRow, startColumn, DEAD_END) return found
该函数做的第一件事就是调用updatePosition(第2行)。这样做是为了对算法进行可视化,以便我们
看到小乌龟如何在迷宫中寻找出口。接着,该函数检查前3中基本情况:是否遇到了墙(第5行)是否遇到
了已经走过的格子(第8行)是否找到了出口(第11行)如果没有一种情况符合,则继续递归搜索。
递归搜素调用了4个searchFrom。很难预测一共会进行多少个递归调用,这是因为它们都是用布尔运
算符or连接起来的。如果第一次调用searchFrom后返回Ture,那么就不必进行后续的调用。可以这样理解:
向北移动一格是离开迷宫的路径的一步。如果向北没有能够走出迷宫,那么尝试下一个递归调用,即向南移
动。如果向南失败了,就尝试向西,最后向东。如果所有的递归调用都失败了,就说明遇到了死胡同。请下
载或自己输入代码,改变4个递归调用的顺序,看看结果如何。
Maze类的方法定义,--init--方法只接受一个参数,即文件名。该文本文件包含迷宫的信息,其中+代表
墙,空格代表空格子,S代表起始位置。迷宫内部表示是一个列表,其元素也是列表。实例变量mazelist的每
一行是一个列表,其中每一格包含一个字符。其内部如下。
drawMaze方法使用以上内部表示在屏幕上绘制初始迷宫。
updatePosition方法使用相同的内部表示检查小乌龟是否遇到墙。同时,它会更改内部表示,使用.和-来分
别表示小乌龟遇到了走过的格子和死胡同。此外,updatePosition方法还使用辅助函数moveTurtle和dropBread
crumb来更新屏幕上的信息。
isExit方法检查小乌龟的当前位置是否为出口,条件是小乌龟已经爬到迷宫边缘:第0行、第0列、最后一行
或者最后一列。
import turtle # 迷宫类 class Maze(object): # 读取迷宫数据,初始化迷宫内部,并找到海龟初始位置。 def __init__(self, mazeFileName): rowsInMaze = 0 #初始化迷宫行数 columnsInMaze = 0 #初始化迷宫列数 self.mazelist = [] #初始化迷宫列表 mazeFile = open(mazeFileName, 'r') #读取迷宫文件 for line in mazeFile: #按行读取 rowList = [] #初始化行列表 col = 0 #初始化列 for ch in line[:-1]: #这样会丢失最后一列 rowList.append(ch) #添加到行列表 if ch == 'S': #S为乌龟初始位置,即迷宫起点 self.startRow = rowsInMaze #乌龟初始行 self.startCol = col #乌龟初始列 col = col + 1 #下一列 rowsInMaze = rowsInMaze + 1 #下一行 self.mazelist.append(rowList) #行列表添加到迷宫列表 columnsInMaze = len(rowList) #获取迷宫总列数 print(self.mazelist) self.rowsInMaze = rowsInMaze #设置迷宫总行数 self.columnsInMaze = columnsInMaze #设置迷宫总列数 self.xTranslate = -columnsInMaze/2 #设置迷宫左上角的初始x坐标 self.yTranslate = rowsInMaze/2 #设置迷宫左上角的初始y坐标 self.t = turtle.Turtle() #创建一个海龟对象 #给当前指示点设置样式(类似鼠标箭头),海龟形状为参数指定的形状名,指定的形状名应存在于TurtleScreen的shape字典中。多边形的形状初始时有以下几种:"arrow", "turtle", "circle", "square", "triangle", "classic"。 self.t.shape('turtle') self.wn = turtle.Screen() #创建一个能在里面作图的窗口 # 设置世界坐标系,原点在迷宫正中心。参数依次为画布左下角x轴坐标、左下角y轴坐标、右上角x轴坐标、右上角y轴坐标 self.wn.setworldcoordinates(-(columnsInMaze -1)/2 - 0.5, -(rowsInMaze - 1)/2 - 0.5, (columnsInMaze - 1)/2 + 0.5, (rowsInMaze - 1)/2 + 0.5) # 在屏幕上绘制迷宫 def drawMaze(self): self.t.speed(20) #绘图速度 for y in range(self.rowsInMaze): #按单元格依次循环迷宫 for x in range(self.columnsInMaze): if self.mazelist[y][x] == OBSTACLE: #如果迷宫列表的该位置为障碍物,则画方块 self.drawCenteredBox(x + self.xTranslate, -y + self.yTranslate, 'orange') # 画方块 def drawCenteredBox(self, x, y, color): self.t.up() #画笔抬起 self.t.goto(x - 0.5, y - 0.5) #前往参数位置,此处0.5偏移量的作用是使乌龟的探索路线在单元格的正中心位置 self.t.color(color) #方块边框为橙色 self.t.fillcolor('green') #方块内填充绿色 self.t.setheading(90) #设置海龟的朝向,标准模式:0 - 东,90 - 北,180 - 西,270 - 南。logo模式:0 - 北,90 - 东,180 - 南,270 - 西。 self.t.down() #画笔落下 self.t.begin_fill() #开始填充 for i in range(4): #画方块边框 self.t.forward(1) #前进1个单位 self.t.right(90) #右转90度 self.t.end_fill() #结束填充 # 移动海龟 def moveTurtle(self, x, y): self.t.up() #画笔抬起 # setheading()设置海龟朝向,towards()从海龟位置到由(x, y),矢量或另一海龟位置连线的夹角。此数值依赖于海龟初始朝向,由"standard"、"world"或"logo" 模式设置所决定。 self.t.setheading(self.t.towards(x + self.xTranslate, -y + self.yTranslate)) self.t.goto(x + self.xTranslate, -y + self.yTranslate) #前往目标位置 # 画路径圆点 def dropBreadcrumb(self, color): self.t.dot(color) #dot(size=None, color)画路径圆点 # 用以更新迷宫内的状态及在窗口中改变海龟位置,行列参数为乌龟的初始坐标。 def updatePosition(self, row, col, val): self.mazelist[row][col] = val #设置该标记状态为当前单元格的值 self.moveTurtle(col, row) #移动海龟 if val == PART_OF_PATH: #其中一条成功路径的圆点的颜色 color = 'green' elif val == TRIED: #尝试用的圆点的颜色 color = 'black' elif val == DEAD_END: #死胡同用的圆点的颜色 color = 'red' self.dropBreadcrumb(color) #画路径圆点并上色 # 用以判断当前位置是否为出口。 def isExit(self, row, col): return (row == 0 or row == self.rowsInMaze - 1 or col == 0 or col == self.columnsInMaze - 1) #根据海龟位置是否在迷宫的4个边线位置判断 # 返回键对应的值,影响searchFrom()中maze[startRow][startColumn]值的获取 def __getitem__(self, key): return self.mazelist[key] if __name__ == '__main__': PART_OF_PATH = 'O' #部分路径 TRIED = '.' #尝试 OBSTACLE = '+' #障碍 DEAD_END = '-' #死胡同 myMaze = Maze('maze.txt')#实例化迷宫类,maze文件是使用“+”字符作为墙壁围出空心正方形空间,并用字母“S”来表示起始位置的迷宫文本文件。 myMaze.drawMaze() #在屏幕上绘制迷宫。 searchFrom(myMaze, myMaze.startRow, myMaze.startCol) #探索迷宫