迷宫问题是机器智能中一种常见的问题,我们在生活中也会常常遇到这类问题:我们会顺着某一方向向前探索,如果遇到岔口,则要选择某一个路口前进,会出现两种可能性,若能走通,则继续往前走,最后顺利通到出口处;否则沿原路退回,换一个方向在继续探索,直至所有可能的通路都探索到为止。
用问题定义可以描述为迷宫是一个M*N的二维矩阵,其中0为墙,1为路,入口在第一列,出口在最后一列。要求从入口开始,从出口结束,按照上,下,左,右的顺序来搜索路径,设计程序,对任意设定的迷宫,求出从入口到出口的所有通路,如图所示。
求解方法
程序调用自身的编程技巧称为递归。递归做为一种算法在程序设计语言中广泛应用。一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的能力在于用有限的语句来定义对象的无限集合。一般来说,递归需要有边界条件、递归前进段和递归返回段。当边界条件不满足时,递归前进;当边界条件满足时,递归返回。
1.2.1基本思路
①每个时刻总有一个当前位置,开始时这个位置是迷宫入口。
②如果当前位置就是出口,问题已解决。
③否则,如果从当前位置己无路可走,当前的探查失败,回退一步。
④取一个可行相邻位置用同样方式探查,如果从那里可以找到通往出口的路径,那么从当前位置到出口的路径也就找到了。
1.2.2算法过程
在整个计算开始时,把迷宫的入口作为检查的当前位置,算法过程就是:
①用mark函数确定当前位置。
②检查当前位置是否为出口,如果是则成功结束。
③逐个检查当前位置的四邻是否可以通达出口,用递归调用自身的函数。
④如果对四邻的探索都失败,报告失败。
1.3.1代码
dirs=[(0,1),(1,0),(0,-1),(-1,0)] #当前位置四个方向的偏移量
path=[] #存找到的路径
def mark(maze,pos): #给迷宫maze的位置pos标"2"表示“倒过了”
maze[pos[0]][pos[1]]=2
def passable(maze,pos): #检查迷宫maze的位置pos是否可通行
return maze[pos[0]][pos[1]]==0
def find_path(maze,pos,end):
mark(maze,pos)
if pos==end:
print(pos,end=" ") #已到达出口,输出这个位置。成功结束
path.append(pos)
return True
for i in range(4): #否则按四个方向顺序检查
nextp=pos[0]+dirs[i][0],pos[1]+dirs[i][1]
#考虑下一个可能方向
if passable(maze,nextp): #不可行的相邻位置不管
if find_path(maze,nextp,end):#如果从nextp可达出口,输出这个位置,成功结束
print(pos,end=" ")
path.append(pos)
return True
return False
def see_path(maze,path): #使寻找到的路径可视化
for i,p in enumerate(path):
if i==0:
maze[p[0]][p[1]] ="E"
elif i==len(path)-1:
maze[p[0]][p[1]]="S"
else:
maze[p[0]][p[1]] =3
print("\n")
for r in maze:
for c in r:
if c==3:
print('\033[0;31m'+"*"+" "+'\033[0m',end="")
elif c=="S" or c=="E":
print('\033[0;34m'+c+" " + '\033[0m', end="")
elif c==2:
print('\033[0;32m'+"#"+" "+'\033[0m',end="")
elif c==1:
print('\033[0;;40m'+" "*2+'\033[0m',end="")
else:
print(" "*2,end="")
print()
if __name__ == '__main__':
maze=[[1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[1,0,0,0,1,1,0,0,0,1,0,0,0,1],
[1,0,1,0,0,0,0,1,0,1,0,1,0,1],
[1,0,1,0,1,1,1,1,0,1,0,1,0,1],
[1,0,1,0,0,0,0,0,0,1,1,1,0,1],
[1,0,1,1,1,1,1,1,1,1,0,0,0,1],
[1,0,1,0,0,0,0,0,0,0,0,1,0,1],
[1,0,0,0,1,1,1,0,1,0,1,1,0,1],
[1,0,1,0,1,0,1,0,1,0,1,0,0,1],
[1,0,1,0,1,0,1,0,1,1,1,1,0,1],
[1,0,1,0,0,0,1,0,0,1,0,0,0,1],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1]]
start=(1,1)
end=(10,12)
find_path(maze,start,end)
see_path(maze,path)
1.3.2结果
运行结果如图所示,在上面给出的迷宫里,找到了15个点,这15个点的坐标就是递归求解方法最后找出的路线。在n*n的迷宫里,因为需要遍历所有的点,时间复杂度就是O(n*n)。
回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。
2.2.1基本思路
迷宫问题主要思路就是从入口开始,顺着路(1)行走,遇墙(0)改变路线,有四个方向,向上向下向左向右,每进入一格都要遍历这4种状态。因此可以根据子集树模板进行,如图所示。这次用到的方法是回溯,用该方法对解空间进行搜索。
2.2.2算法过程
if 到达了目的地:
打印结果矩阵
else:
if 此位置已经走过了:
返回上一步
else:
尝试向四周走,递归此核心思路
2.3.1代码
class Maze():
# 初始化迷宫的规格大小
def __init__(self, n):
self.n = n
# 初始化结果矩阵
self.flag = [[0 for _ in range(n)] for _ in range(n)]
# 判断位置是否合法,如果可以走,那么返回True,否则返回False
def isSafe(self, maze, x, y):
return x >= 0 and x <= self.n - 1 and y >= 0 and y <= self.n - 1 and maze[x][y] == 1
# 打印矩阵
def printSolution(self, flag):
for i in flag:
for j in i:
print('%4s' % str(j), end='')
print('\n')
# 获取走出迷宫的结果矩阵
def getPath(self, maze, x, y):
# 如果此处位置合法但是此处走过了,返回到上一步
if self.isSafe(maze, x, y) and self.flag[x][y] == '-':
return False
# 如果到达了目的地,返回True
if x == self.n - 1 and y == self.n - 1:
self.flag[x][y] = '-'
self.printSolution(self.flag)
return True
# 没有到达目的地,继续往下走
if self.isSafe(maze, x, y):
# 标记结果矩阵中当前位置为-
self.flag[x][y] = '-'
# 尝试向右走,递归
if self.getPath(maze, x + 1, y):
return True
# 尝试向下走,递归
if self.getPath(maze, x, y + 1):
return True
# 尝试向左走,递归
if self.getPath(maze, x - 1, y):
return True
# 尝试向上走,递归
if self.getPath(maze, x, y - 1):
return True
# 此路不通,标记当前的单元为0,表示此处不可能到达目的地,返回上一步
self.flag[x][y] = 0
return False
# 此位置不合法,返回False
return False
if __name__ == '__main__':
maze = [[1, 1, 0, 1, 1, 1, 1, 1, 0],
[0, 1, 0, 1, 0, 1, 0, 1, 1],
[0, 1, 1, 1, 0, 1, 0, 0, 0],
[0, 1, 0, 0, 1, 1, 0, 0, 0],
[0, 1, 1, 1, 1, 0, 1, 1, 1],
[0, 0, 1, 0, 0, 0, 1, 0, 1],
[1, 1, 1, 1, 1, 1, 1, 0, 1],
[0, 1, 1, 0, 0, 0, 0, 0, 1],
[1, 0, 1, 0, 0, 0, 0, 0, 1], ]
rat = Maze(len(maze))
ret = rat.getPath(maze, 0, 0)
if ret == False:
print("No solution!")
2.3.2结果
用回溯法求解迷宫问题运行结果如图所示,其中“-”表示的就是通路,用回溯法求解迷宫问题还是需要遍历到每一个位置,因此时间复杂度还是O(n*n)。
3.1队列
队列是一种特殊的线性表,特殊之处在于它只允许在表的前端进行删除操作,而在表的后端进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。队列中没有元素时,称为空队列。
队列的数据元素又称为队列元素。在队列中插入一个队列元素称为入队,从队列中删除一个队列元素称为出队。因为队列只允许在一端插入,在另一端删除,所以只有最早进入队列的元素才能最先从队列中删除,故队列又称为先进先出线性表。
3.2.1基本思路
队列求解算法中,以队列存储可以探索的位置。利用队列先进先出的特点,实现在每个分支上同时进行搜索路径,直到找到出口。这是一种广度优先搜索的方法。
3.2.2算法思路
首先构建一个空队列,手动输入一个迷宫,进行下一步,搜索通路,有通路直至到达终点,无通路就退回到起点。搜索过程中,遇到前面不再有出口的时候,就要使当前队尾结点出队,于是就要对当前队列从队头到倒数第二个结点依次转移,释放出队尾结点。
3.3.1代码
from collections import deque
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]
]
dirs = [
lambda x,y:(x+1,y),#下
lambda x,y:(x,y-1),#左
lambda x,y:(x,y+1),#右
lambda x,y:(x-1,y),#上
]
def print_r(path):
curNode = path[-1]
realpath = []
while curNode[2] != -1:
realpath.append(curNode[0:2])
curNode = path[curNode[2]]
realpath.append(curNode[0:2]) # 把起点放进去
realpath.reverse()
print(len(realpath))
for i in realpath:
print(i)
def maze_path_queue(x1,y1,x2,y2):
maze[x1][y1]=2
queue = deque()
queue.append((x1,y1,-1))
path = []
while len(queue)>0:
curNode = queue.popleft()
path.append(curNode)
if curNode[0] == x2 and curNode[1] == y2:
print_r(path)
return True
for dir in dirs:
nextNode = dir(curNode[0],curNode[1])
if maze[nextNode[0]][nextNode[1]]==0:
# 后续节点进队,记录哪个节点带他来的
node = (nextNode[0],nextNode[1],len(path)-1)
queue.append(node)
maze[nextNode[0]][nextNode[1]] = 2 # 标记为已经走过
else:
print('没有路')
return False
maze_path_queue(1,1,8,8)
3.3.2结果
如图所示,这是队列求解最后的结果,需要遍历每个位置,时间复杂度为O(n*n)。