前言:此篇文章只是小菜鸟的自我练习,如有错误还请大家指出!
迷宫问题一直是一个十分经典的问题,其中涉及了一些深度优先搜索和广度优先搜索的知识,说白了其无非就是两个待解决的问题,一是如何找到出路,而是如何找到最短出路?
迷宫问题
问题一:迷宫的出路(用栈来解决问题)
如图,这是一个迷宫,首先我们需要用二位列表模拟出一个迷宫,墙(即走不通的地方)标记为1,能走的路标记为0,代码如下图所示:
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]
] # 迷宫的构建
由于问题只需要我们找到出路即可,并不需要找到路最短的出路,因此我们选择使用深度优先遍历思想,即每走到一个点时,就判断其四个方向的位置是否能走,若有一个能走,就前往那个方向,直到找到出路,或者是这条路走到死胡同,同时每走一步都需要将走过的路标记为2,方便走到死胡同或是走到出路来回溯之前的路。若走到死胡同那么便依次沿着道路回退即可,如下图:
回退时一旦回退到上一个点就需要判断上一个点有没有别的路可以走,走路的四个方向的顺序由自己决定,在这个问题中定的方向不同会有不同的出路!
接下来要实现方向列表,为了避免每一次上下左右的撰写,我们将方向以匿名函数分装到列表的形式来实现方向的变换,代码如下:
dirs = [
lambda x, y: (x + 1, y),
lambda x, y: (x - 1, y),
lambda x, y: (x, y + 1),
lambda x, y: (x, y - 1),
] # 四大方向,以右左上下的顺序
至此,寻找出路前的准备工作就结束了,接下来进入正式的实现部分。
首先思考函数的参数,找出路,我们必定需要起点和终点的坐标,因此,以x1,y1为起点,x2,y2为终点,传入的四个参数即为起点和终点的坐标,按照上述的思路,以我们给定的方向顺序去探索若能走就直接走,走到死路或走到终点就回溯,回溯的时候需要删除已经走过的路,此外还需要考虑极端情况,即这个迷宫根本就走不出去,这种情形下,走每一条路都是死路,那回溯到最后栈内将不会有元素,因此我们循环结束的条件就是栈内不为空。
def maze_path(x1,y1,x2,y2):
solution = [] # 存放路的列表
solution.append((x1,y1)) # 将起点入进列表
while len(solution) > 0: # 循环终止的条件
curNode = solution[-1] # 定义当前走到的结点
if curNode[0] == x2 and curNode[1] == y2: # 找到路了
print("找到出路了!")
for key in solution:
print(key) # 此时,solution中的就是出路,打印即可
return True
for dir in dirs: # 从当前节点四个方向找路
nextNode = dir(curNode[0],curNode[1])
if maze[nextNode[0]][nextNode[1]] == 0: # 若能走就走
solution.append(nextNode)
maze[nextNode[0]][nextNode[1]] = 2 # 标记走过的路为2
break # 找到一条能走的就走
else: # 四个方向一个都走不了,死路
maze[nextNode[0]][nextNode[1]] = 2 # 标记走过的路为2
solution.pop(-1) # 删去走过的路
else: # 栈内没有元素,找不到路
print("找不到路")
return False
代码如上图所示,注意点都在图上,注意点就是标记走过的路,且为死路时就删除走过的路。
问题二:寻找最佳出路(用双向队列来解决问题)
仍然是那个迷宫,但这次我们的目的是找到路最短的出路,这个时候我们需要运用到广度优先搜索的思想,即几条路同时走,直到有一条路找到了终点,同时为了回溯那一条找到终点的路,我们需要用三维坐标来记录迷宫中的点,前两个坐标是正常的二位列表中的位置,第三个坐标是记录此结点的上一个节点的下标,起始点默认下标为-1,通过下标即可回溯道路。
举个具体的例子来说,1点是由下标为-1的点带来的,因此其坐标为(x,y,-1),3点是由下标为1的点带来的,其坐标为(x,y,1),而4和5都是由3带来的。
因此我们需要path存所有的三维坐标,而queue里存放的是逐渐推进的路程,即能走就存进去,每一次前进删去前面的路,在下图中体现就是走到3点时,先存4点,再存5点,然后将3点删除,此时就有3引出来的两条路可以走,先走4,将4点删除,将4的下一个点6带进来,将5删去将5的下一个7带进来,直到找到终点。这样便能达到同时搜索同时走的效果,就可以知道哪条路最快可以找到终点。
from collections import deque
def print_f(path): # 回溯
curNode = path[-1] # curNode从终点开始回溯
realpath = [] # 记录真实路径的列表
while curNode[2] != -1: # 当还没有回溯到起点的时候,就一直循环
realpath.append((curNode[0], curNode[1]))
curNode = path[curNode[2]] # 根据下标回溯即可
realpath.append((curNode[0], curNode[1])) # 结束循环后,curNode已经是起点了
realpath.reverse() # 将真实路径倒序
for line in realpath:
print(line) # 打印通路
def maze_path_queue(x1, y1, x2, y2):
queue = deque() # 构建双向队列
queue.append((x1, y1, -1))
path = []
while len(queue) > 0:
curNode = queue.popleft() # queue里面是实时更新的路,几条路同时走
path.append(curNode) # path里存放的是走过的点的坐标以及父亲的下标
if curNode[0] == x2 and curNode[1] == y2: # 此时path内的最后一个就是终点和终点前一个点的下标
print_f(path) # 回溯打印最短通路
return True
for dir in dirs: # 4条路能走就都走,不需要找到一条就break
nextNode = dir(curNode[0], curNode[1])
if maze[nextNode[0]][nextNode[1]] == 0:
maze[nextNode[0]][nextNode[1]] = 2
queue.append((nextNode[0], nextNode[1], len(path) - 1))
else:
print("找不到路")
return False
测试部分:
maze_path(1,1,8,8)
maze_path_queue(1,1,8,8)
总结,迷宫问题作为简单的入门问题,需要仔细琢磨,并延伸至其他题目,希望大家多多练习,若想补充栈和队列的相关知识,可以参考我的前两篇文章。
python数据结构之队列实现-CSDN博客 --------队列
python数据结构之栈的实现-CSDN博客 --------栈