• 4.11.探索迷宫

    4.11.探索迷宫

    在这一节中,我们将讨论一个与扩展机器人世界相关的问题:你如何找到自己的迷宫? 如果你在你的宿舍有一个扫地机器人(不是所有的大学生?)你希望你可以使用你在本节中学到的知识重新给它编程。 我们要解决的问题是帮助我们的乌龟在虚拟迷宫中找到出路。 迷宫问题的根源与希腊的神话有关,传说忒修斯被送入迷宫中以杀死人身牛头怪。忒修斯用了一卷线帮助他找到回去的退路,当他完成杀死野兽的任务。在我们的问题中,我们将假设我们的乌龟在迷宫中间的某处,必须找到出路。 看看 Figure 2,了解我们将在本节中做什么。

    4.11.探索迷宫.figure2

    Figure 2

    为了使问题容易些,我们假设我们的迷宫被分成“正方形”。迷宫的每个正方形是开放的或被一段墙壁占据。乌龟只能通过迷宫的空心方块。 如果乌龟撞到墙上,它必须尝试不同的方向。乌龟将需要一个程序,以找到迷宫的出路。这里是过程:

    • 从我们的起始位置,我们将首先尝试向北一格,然后从那里递归地尝试我们的程序。
    • 如果我们通过尝试向北作为第一步没有成功,我们将向南一格,并递归地重复我们的程序。
    • 如果向南也不行,那么我们将尝试向西一格,并递归地重复我们的程序。
    • 如果北,南和西都没有成功,则应用程序从当前位置递归向东。
    • 如果这些方向都没有成功,那么没有办法离开迷宫,我们失败。现在,这听起来很容易,但有几个细节先谈谈。假设我们第一步是向北走。按照我们的程序,我们的下一步也将是向北。但如果北面被一堵墙阻挡,我们必须看看程序的下一步,并试着向南。不幸的是,向南使我们回到原来的起点。如果我们从那里再次应用递归过程,我们将又回到向北一格,并陷入无限循环。所以,我们必须有一个策略来记住我们去过哪。在这种情况下,我们假设有一袋面包屑可以撒在我们走过的路上。如果我们沿某个方向迈出一步,发现那个位置上已经有面包屑,我们应该立即后退并尝试程序中的下一个方向。我们看看这个算法的代码,就像从递归函数调用返回一样简单。

    正如我们对所有递归算法所做的一样,让我们回顾一下基本情况。其中一些你可能已经根据前一段的描述猜到了。在这种算法中,有四种基本情况要考虑:

    • 乌龟撞到了墙。由于这一格被墙壁占据,不能进行进一步的探索。
    • 乌龟找到一个已经探索过的格。我们不想继续从这个位置探索,否则会陷入循环。
    • 我们发现了一个外边缘,没有被墙壁占据。换句话说,我们发现了迷宫的一个出口。
    • 我们探索了一格在四个方向上都没有成功。为了我们的程序工作,我们将需要有一种方式来表示迷宫。为了使这个更有趣,我们将使用 turtle 模块来绘制和探索我们的迷宫,以使我们看到这个算法的功能。迷宫对象将提供以下方法让我们在编写搜索算法时使用:
    • init 读取迷宫的数据文件,初始化迷宫的内部表示,并找到乌龟的起始位置。
    • drawMaze 在屏幕上的一个窗口中绘制迷宫。
    • updatePosition 更新迷宫的内部表示,并更改窗口中乌龟的位置。
    • isExit 检查当前位置是否是迷宫的退出位置。Maze 类还重载索引运算符 [] ,以便我们的算法可以轻松访问任何特定格的状态。

    让我们来查看称为 searchFrom 的搜索函数的代码。代码如 Listing 3 所示。请注意,此函数需要三个参数:迷宫对象,起始行和起始列。 这很重要,因为作为递归函数,搜索在每次递归调用时开始。

    1. def searchFrom(maze, startRow, startColumn):
    2. maze.updatePosition(startRow, startColumn)
    3. # Check for base cases:
    4. # 1. We have run into an obstacle, return false
    5. if maze[startRow][startColumn] == OBSTACLE :
    6. return False
    7. # 2. We have found a square that has already been explored
    8. if maze[startRow][startColumn] == TRIED:
    9. return False
    10. # 3. Success, an outside edge not occupied by an obstacle
    11. if maze.isExit(startRow,startColumn):
    12. maze.updatePosition(startRow, startColumn, PART_OF_PATH)
    13. return True
    14. maze.updatePosition(startRow, startColumn, TRIED)
    15. # Otherwise, use logical short circuiting to try each
    16. # direction in turn (if needed)
    17. found = searchFrom(maze, startRow-1, startColumn) or \
    18. searchFrom(maze, startRow+1, startColumn) or \
    19. searchFrom(maze, startRow, startColumn-1) or \
    20. searchFrom(maze, startRow, startColumn+1)
    21. if found:
    22. maze.updatePosition(startRow, startColumn, PART_OF_PATH)
    23. else:
    24. maze.updatePosition(startRow, startColumn, DEAD_END)
    25. return found

    Listing 3

    你会看到代码的第一行(行 2)调用 updatePosition。这只是为了可视化算法,以便你可以看到乌龟如何探索通过迷宫。接下来算法检查四种基本情况中的前三种:乌龟是否碰到墙(行 5)?乌龟是否回到已经探索过的格子(行 8)?乌龟有没有到达出口(行 11)?如果这些条件都不为真,则我们继续递归搜索。

    你会注意到,在递归步骤中有四个对 searchFrom 的递归调用。很难预测将有多少个递归调用,因为它们都由 or 语句连接。如果对 searchFrom 的第一次调用返回 True ,则不需要最后三个调用。你可以理解这一步向 (row-1,column)(或北,如果你从地理位置上思考)是在迷宫的路径上。如果没有一个好的路径向北,那么尝试下一个向南的递归调用。如果向南失败,然后尝试向西,最后向东。如果所有四个递归调用返回 False,那么认为是一个死胡同。你应该下载或输入整个程序,并通过更改这些调用的顺序进行实验。

    Maze 类的代码如 Listing 4,Listing 5 和 Listing 6 所示。init 方法将文件的名称作为其唯一参数。此文件是一个文本文件,通过使用 + 字符表示墙壁,空格表示空心方块,并使用字母 S 表示起始位置。Figure 3 是迷宫数据文件的示例。迷宫的内部表示是列表的列表。 mazelist 实例变量的每一行也是一个列表。此辅助列表使用上述字符,每格表示一个字符。Figure 3 中的数据文件,内部表示如下所示:

    1. [ ['+','+','+','+',...,'+','+','+','+','+','+','+'],
    2. ['+',' ',' ',' ',...,' ',' ',' ','+',' ',' ',' '],
    3. ['+',' ','+',' ',...,'+','+',' ','+',' ','+','+'],
    4. ['+',' ','+',' ',...,' ',' ',' ','+',' ','+','+'],
    5. ['+','+','+',' ',...,'+','+',' ','+',' ',' ','+'],
    6. ['+',' ',' ',' ',...,'+','+',' ',' ',' ',' ','+'],
    7. ['+','+','+','+',...,'+','+','+','+','+',' ','+'],
    8. ['+',' ',' ',' ',...,'+','+',' ',' ','+',' ','+'],
    9. ['+',' ','+','+',...,' ',' ','+',' ',' ',' ','+'],
    10. ['+',' ',' ',' ',...,' ',' ','+',' ','+','+','+'],
    11. ['+','+','+','+',...,'+','+','+',' ','+','+','+']]

    drawMaze 方法使用这个内部表示在屏幕上绘制迷宫的初始视图。

    Figure 3:示例迷宫数据文件

    1. ++++++++++++++++++++++
    2. + + ++ ++ +
    3. + + + +++ + ++
    4. + + + ++ ++++ + ++
    5. +++ ++++++ +++ + +
    6. + ++ ++ +
    7. +++++ ++++++ +++++ +
    8. + + +++++++ + +
    9. + +++++++ S + +
    10. + + +++
    11. ++++++++++++++++++ +++

    Figure 3

    如 Listing 5 所示,updatePosition 方法使用相同的内部表示来查看乌龟是否遇到了墙。它还用 .- 更新内部表示,以表示乌龟已经访问了特定格子或者格子是死角。 此外,updatePosition 方法使用两个辅助方法moveTurtledropBreadCrumb 来更新屏幕上的视图。

    最后,isExit 方法使用乌龟的当前位置来检测退出条件。退出条件是当乌龟已经到迷宫的边缘时,即行零或列零,或者在最右边列或底部行。

    1. class Maze:
    2. def __init__(self,mazeFileName):
    3. rowsInMaze = 0
    4. columnsInMaze = 0
    5. self.mazelist = []
    6. mazeFile = open(mazeFileName,'r')
    7. rowsInMaze = 0
    8. for line in mazeFile:
    9. rowList = []
    10. col = 0
    11. for ch in line[:-1]:
    12. rowList.append(ch)
    13. if ch == 'S':
    14. self.startRow = rowsInMaze
    15. self.startCol = col
    16. col = col + 1
    17. rowsInMaze = rowsInMaze + 1
    18. self.mazelist.append(rowList)
    19. columnsInMaze = len(rowList)
    20. self.rowsInMaze = rowsInMaze
    21. self.columnsInMaze = columnsInMaze
    22. self.xTranslate = -columnsInMaze/2
    23. self.yTranslate = rowsInMaze/2
    24. self.t = Turtle(shape='turtle')
    25. setup(width=600,height=600)
    26. setworldcoordinates(-(columnsInMaze-1)/2-.5,
    27. -(rowsInMaze-1)/2-.5,
    28. (columnsInMaze-1)/2+.5,
    29. (rowsInMaze-1)/2+.5)

    Listing 4

    1. def drawMaze(self):
    2. for y in range(self.rowsInMaze):
    3. for x in range(self.columnsInMaze):
    4. if self.mazelist[y][x] == OBSTACLE:
    5. self.drawCenteredBox(x+self.xTranslate,
    6. -y+self.yTranslate,
    7. 'tan')
    8. self.t.color('black','blue')
    9. def drawCenteredBox(self,x,y,color):
    10. tracer(0)
    11. self.t.up()
    12. self.t.goto(x-.5,y-.5)
    13. self.t.color('black',color)
    14. self.t.setheading(90)
    15. self.t.down()
    16. self.t.begin_fill()
    17. for i in range(4):
    18. self.t.forward(1)
    19. self.t.right(90)
    20. self.t.end_fill()
    21. update()
    22. tracer(1)
    23. def moveTurtle(self,x,y):
    24. self.t.up()
    25. self.t.setheading(self.t.towards(x+self.xTranslate,
    26. -y+self.yTranslate))
    27. self.t.goto(x+self.xTranslate,-y+self.yTranslate)
    28. def dropBreadcrumb(self,color):
    29. self.t.dot(color)
    30. def updatePosition(self,row,col,val=None):
    31. if val:
    32. self.mazelist[row][col] = val
    33. self.moveTurtle(col,row)
    34. if val == PART_OF_PATH:
    35. color = 'green'
    36. elif val == OBSTACLE:
    37. color = 'red'
    38. elif val == TRIED:
    39. color = 'black'
    40. elif val == DEAD_END:
    41. color = 'red'
    42. else:
    43. color = None
    44. if color:
    45. self.dropBreadcrumb(color)

    Listing 5

    1. def isExit(self,row,col):
    2. return (row == 0 or
    3. row == self.rowsInMaze-1 or
    4. col == 0 or
    5. col == self.columnsInMaze-1 )
    6. def __getitem__(self,idx):
    7. return self.mazelist[idx]

    Listing 6