老鼠走迷宫与回溯
老鼠走迷宫是在网格上寻找从入口到出口的路径。回溯法沿一条路走到底,遇到死路就退回上一个分叉,换方向再试。本页说明迷宫的表示、四个方向的搜索顺序、访问标记和失败标记,并给出完整实现。回溯的逐层调用与返回依赖递归;若要找最短路径或比较不同路径的代价,应改用广度优先搜索或最短路径算法,而不只是找一条可行路径。
定义
回溯法按顺序尝试候选解:选择一个候选后递归进入下一层;若沿这个选择无法到达目标,就放弃它,回到上一层尝试下一个候选。与单纯递归枚举的区别是失败后要处理这个候选留下的状态——恢复原状,或标记为已确认不可行,让其他分支能在正确的局面上继续。
用二维列表m表示迷宫,每个格子取以下值之一:
0:可以走的格子;1:墙;2:当前路径上的格子,进入时标记;3:已经确认从这里走不到出口的格子,回溯时标记。
入口是 (sta1, sta2),出口是 (fsh1, fsh2)。每一步只能走上、下、左、右四个相邻格子,不能斜走。
性质
搜索步骤
-
标记当前格子
进入
(i, j)后先令m[i][j] = 2,表示这个格子已经访问,防止搜索绕回自己。 -
检查是否到达出口
若
(i, j)就是出口,令全局标志success = 1,随后逐层返回。 -
按固定顺序尝试四个方向
依次检查上、下、左、右。若相邻格子为0且尚未找到出口,就递归进入该格子。
-
失败则标记并返回
四个方向都没有找到出口时,把
m[i][j]改为3,表示从这里出发走不通,再返回上一层。
标记的作用
2和3都让格子不再以0的身份被再次进入,但含义不同:
2是当前搜索路径的一部分。搜索还在进行时,它阻止回到路径上的格子,避免在环里无限递归。3是已经验证过的死路。从别的方向到达同一个格子,结果仍然是走不通,所以可以直接跳过。
找到一条路径就停止
success是全局标志。一旦某次递归到达出口,其余方向不再尝试,所有调用依次返回。这样得到的是一条可行路径,不保证最短。若要找最短路径,应使用按层扩展的广度优先搜索;若要枚举所有路径,失败后必须把格子恢复为0,而不是标成3。
边界与代价
代码依赖迷宫最外圈全是1:向上、向左越界时,Python的负索引取到的是另一侧的墙;向下越界不会发生,因为最底行是墙,搜索不会进入;这个迷宫的最右列除出口外都是墙,而到达出口后success已经置1,后续方向检查被短路。
每个格子最多被访问一次,时间复杂度为 ( 行、 列);递归深度最多等于路径长度,最坏情况下为 ,大迷宫可能超过Python的默认递归深度。
例子
完整实现:找到一条通路
m = [ [1, 1, 1, 0, 1, 1, 1, 1, 1, 1], [1, 0, 0, 0, 0, 0, 0, 0, 1, 1], [1, 0, 1, 1, 1, 1, 1, 0, 0, 1], [1, 0, 1, 0, 0, 0, 0, 1, 0, 1], [1, 0, 1, 0, 1, 1, 0, 0, 0, 1], [1, 0, 0, 1, 1, 0, 1, 0, 1, 1], [1, 1, 1, 1, 0, 0, 0, 0, 1, 1], [1, 0, 0, 0, 0, 1, 1, 1, 0, 0], [1, 0, 1, 1, 0, 0, 0, 0, 0, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],]
sta1 = 0sta2 = 3fsh1 = 7fsh2 = 9success = 0
def LabyrinthRat(): print("显示迷宫:") for i in range(len(m)): print(m[i]) print("入口:m[%d][%d]:出口:m[%d][%d]" % (sta1, sta2, fsh1, fsh2)) if (visit(sta1, sta2)) == 0: print("没有找到出口") else: print("显示路径:") for i in range(10): print(m[i])
def visit(i, j): m[i][j] = 2 global success if (i == fsh1) and (j == fsh2): success = 1 if (success != 1) and (m[i - 1][j] == 0): visit(i - 1, j) if (success != 1) and (m[i + 1][j] == 0): visit(i + 1, j) if (success != 1) and (m[i][j - 1] == 0): visit(i, j - 1) if (success != 1) and (m[i][j + 1] == 0): visit(i, j + 1) if success != 1: m[i][j] = 3 return success
if __name__ == "__main__": LabyrinthRat()程序结束后,m中值为2的格子连成从入口到出口的路径,值为3的格子是搜索过程中试过但走不通的分支。对这个迷宫,路径从 (0,3) 进入,先向下再向右,绕过若干死路后到达 (7,9)。
LabyrinthRat输出路径时的range(10) 与迷宫行数绑定;换一份尺寸不同的迷宫,这个数字要一起改。
交互探索
逐步查看搜索如何进入相邻格子、在死路回退,并保留访问标记。
走到死路后,搜索怎样回退?
逐步观察深度优先搜索在迷宫中的访问和回溯。
正在准备演示…
练习
在迷宫的标记中,3表示什么?
参考解答
进入格子时标2;四个方向都失败后改成3,表示这个格子出发无解。
为什么要在进入格子后立刻令
m[i][j] = 2?参考解答
标记后该格子不再是0,其他分支不会再次递归进入它;如果没有标记,两个相邻空格会互相调用,搜索无法终止。
对例子中的迷宫,从入口
(0,3)出发,程序最终能否到达出口(7,9)?参考解答
迷宫中存在一条通路,程序会找到它并令
success = 1。找到出口后,全局变量
success的值是 。参考解答
到达出口时执行
success = 1,后续调用据此停止尝试其他方向。如果要求从入口到出口的最短路径,应该改用哪种搜索?
参考解答
回溯只保证找到一条可行路径;搜索顺序不影响路径长度,第一条找到的路径不一定最短。广度优先搜索按步数从少到多扩展,第一次到达出口时的路径最短。