探索迷宫
本节探讨一个与蓬勃发展的机器人领域相关的问题:走出迷宫。如果你有一个 Roomba 扫地机器人,或许能利用在本节学到的知识对它进行重新编程。我们要解决的问题是帮助小乌龟走出虚拟的迷宫。迷宫问题源自忒修斯大战牛头怪的古希腊神话传说。相传,在迷宫里杀死牛头怪之后,忒修斯用一个线团找到了迷宫的出口。本节假设小乌龟被放置在迷宫里的某个位置,我们要做的是帮助它爬出迷宫,如图4-12 所示。

为简单起见,假设迷宫被分成许多格,每一格要么是空的,要么被墙堵上。小乌龟只能沿着空的格子爬行,如果遇到墙,就必须转变方向。它需要如下的系统化过程来找到出路。
-
从起始位置开始,首先向北移动一格,然后在新的位置再递归地重复本过程。
-
如果第一步往北行不通,就尝试向南移动一格,然后递归地重复本过程。
-
如果向南也行不通,就尝试向西移动一格,然后递归地重复本过程。
-
如果向北、向南和向西都不行,就尝试向东移动一格,然后递归地重复本过程。
-
如果 4 个方向都不行,就意味着没有出路。
整个过程看上去非常简单,但是有许多细节需要讨论。假设递归过程的第一步是向北移动一格。根据上述过程,下一步也是向北移动一格。但是,如果北面有墙,必须根据递归过程的第二步向南移动一格。不幸的是,向南移动一格之后回到了起点。如果继续执行该递归过程,就会又向北移动一格,然后又退回来,从而陷入无限循环中。所以,必须通过一个策略来记住到过的地方。本例假设小乌龟一边爬,一边丢面包屑。如果往某个方向走一格之后发现有面包屑,就知道应该立刻退回去,然后尝试递归过程的下一步。查看这个算法的代码时会发现,退回去就是从递归函数调用中返回。
和考察其他递归算法时一样,让我们来看看上述算法的基本情况,其中一些可以根据之前的描述猜到。这个算法需要考虑以下 4 种基本情况。
-
小乌龟遇到了墙。由于格子被墙堵上,因此无法再继续探索。
-
小乌龟遇到了已经走过的格子。在这种情况下,我们不希望它继续探索,不然会陷入循环。
-
小乌龟找到了出口。
-
四个方向都行不通。
为了使程序运行起来,需要通过一种方式表示迷宫。我们使用 turtle 模块来绘制和探索迷宫,以增加趣味性。迷宫对象提供下列方法,可用于编写搜索算法。
-
__init__
读入一个代表迷宫的数据文件,初始化迷宫的内部表示,并且找到小乌龟的起始位置。 -
drawMaze 在屏幕上的一个窗口中绘制迷宫。
-
updatePosition 更新迷宫的内部表示,并且修改小乌龟在迷宫中的位置。
-
isExit 检查小乌龟的当前位置是否为迷宫的出口。
除此之外,Maze 类还重载了索引运算符[],以便算法访问任一格的状态。
代码清单4-10 展示了搜索函数 searchFrom 的代码。该函数接受 3 个参数:迷宫对象、起始行,以及起始列。由于该函数的每一次递归调用在逻辑上都是重新开始搜索的,因此定义成接受 3 个参数非常重要。
def searchFrom(maze, startRow, startColumn):
maze.updatePosition(startRow, startColumn)
# Check for base cases:
# 1. We have run into an obstacle, return false
if maze[startRow][startColumn] == OBSTACLE :
return False
# 2. We have found a square that has already been explored
if maze[startRow][startColumn] == TRIED:
return False
# 3. Success, an outside edge not occupied by an obstacle
if maze.isExit(startRow,startColumn):
maze.updatePosition(startRow, startColumn, PART_OF_PATH)
return True
maze.updatePosition(startRow, startColumn, TRIED)
# Otherwise, use logical short circuiting to try each
# direction in turn (if needed)
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:
maze.updatePosition(startRow, startColumn, DEAD_END)
return found
该函数做的第一件事就是调用 updatePosition(第 2 行)。这样做是为了对算法进行可视化,以便我们看到小乌龟如何在迷宫中寻找出口。接着,该函数检查前3种基本情况:是否遇到了墙(第 5 行)?是否遇到了已经走过的格子(第 8 行)?是否找到了出口(第 11 行)?如果没有一种情况符合,则继续递归搜索。
递归搜索调用了 4 个 searchFrom。很难预测一共会进行多少个递归调用,这是因为它们都是用布尔运算符 or 连接起来的。如果第一次调用 searchFrom 后返回 True,那么就不必进行后续的调用。可以这样理解:向北移动一格是离开迷宫的路径上的一步。如果向北没有能够走出迷宫,那么就会尝试下一个递归调用,即向南移动。如果向南失败了,就尝试向西,最后向东。如果所有的递归调用都失败了,就说明遇到了死胡同。请下载或自己输入代码,改变 4 个递归调用的顺序,看看结果如何。
Maze 类的方法定义如代码清单4-11~代码清单4-13所示。__init__
方法只接受一个参数,即文件名。该文本文件包含迷宫的信息,其中 +
代表墙,空格代表空的格子,S
代表起始位置。图4-13 是迷宫数据文件的例子。迷宫的内部表示是一个列表,其元素也是列表。实例变量 mazelist 的每一行是一个列表,其中每一格包含一个字符。对于图4-13 对应的数据文件,其内部表示如下。

drawMaze 方法使用以上内部表示在屏幕上绘制初始的迷宫,如图4-12 所示。
updatePosition 方法使用相同的内部表示检查小乌龟是否遇到了墙。同时,它会更改内部表示,使用 .
和 -
来分别表示小乌龟遇到了走过的格子和死胡同。此外,updatePosition 方法还使用辅助函数 moveTurtle 和 dropBreadcrumb 来更新屏幕上的信息。
isExit 方法检查小乌龟的当前位置是否为出口,条件是小乌龟已经爬到迷宫边缘:第 0 行、第 0 列、最后一行或者最后一列。
class Maze:
def __init__(self,mazeFileName):
rowsInMaze = 0
columnsInMaze = 0
self.mazelist = []
mazeFile = open(mazeFileName,'r')
rowsInMaze = 0
for line in mazeFile:
rowList = []
col = 0
for ch in line[:-1]:
rowList.append(ch)
if ch == 'S':
self.startRow = rowsInMaze
self.startCol = col
col = col + 1
rowsInMaze = rowsInMaze + 1
self.mazelist.append(rowList)
columnsInMaze = len(rowList)
self.rowsInMaze = rowsInMaze
self.columnsInMaze = columnsInMaze
self.xTranslate = -columnsInMaze/2
self.yTranslate = rowsInMaze/2
self.t = Turtle(shape='turtle')
setup(width=600,height=600)
setworldcoordinates(-(columnsInMaze-1)/2-.5,
-(rowsInMaze-1)/2-.5,
(columnsInMaze-1)/2+.5,
(rowsInMaze-1)/2+.5)
def drawMaze(self):
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,
'tan')
self.t.color('black','blue')
def drawCenteredBox(self,x,y,color):
tracer(0)
self.t.up()
self.t.goto(x-.5,y-.5)
self.t.color('black',color)
self.t.setheading(90)
self.t.down()
self.t.begin_fill()
for i in range(4):
self.t.forward(1)
self.t.right(90)
self.t.end_fill()
update()
tracer(1)
def moveTurtle(self,x,y):
self.t.up()
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)
def updatePosition(self,row,col,val=None):
if val:
self.mazelist[row][col] = val
self.moveTurtle(col,row)
if val == PART_OF_PATH:
color = 'green'
elif val == OBSTACLE:
color = 'red'
elif val == TRIED:
color = 'black'
elif val == DEAD_END:
color = 'red'
else:
color = None
if color:
self.dropBreadcrumb(color)
def isExit(self,row,col):
return (row == 0 or
row == self.rowsInMaze-1 or
col == 0 or
col == self.columnsInMaze-1 )
def __getitem__(self,idx):
return self.mazelist[idx]