前言
如果我想要用一群人来走迷宫,人的总数确定,从一点出发,每到一个节点就分出去一个人,那么我就可以根据要探索的层的数量来判断实际所需要的人数,应该是呈现一个2为底数的幂指数关系,进一步来说,假如迷宫终点是一个宝藏,当其中一个人找到了宝藏,迷宫立刻出现一个随机的变化成为结构不同的新迷宫,由于现在的人已经分散到了迷宫的不同位置,似乎就没办法遵循一群人走迷宫的策略了,为了能够在新的迷宫中找到宝藏,要采取怎样的策略,显然仅仅采用BFS是难以解决的
然而,BFS对于迷宫问题仍有其优势,在上一篇随笔中,学习了DFS,然而DFS不适用于迷宫有孤岛的情形,正如遵循右手定则走迷宫一样,容易在一个路径循环绕圈而找不到出口,所以在此首先学一下可解决孤岛的BFS
算法学习(Breadth First Depth)
算法原理
BFS(广度优先搜索)是一种用于图搜索的算法,它从起始节点开始,逐层遍历图中的节点,直到找到目标节点或遍历完所有节点。BFS使用队列来存储待访问的节点,每次从队列中取出一个节点,并将其所有未访问的邻居节点加入队列。
相比DFS,BFS的遍历顺序具有“先进先出”的特性
算法步骤
- 将起始节点加入队列,并标记为已访问。
- 当队列不为空时,执行以下步骤:
a. 从队列中取出队首节点,访问该节点。
b. 如果该节点是目标节点,则搜索结束,返回结果。
c. 否则,并将其所有未访问的邻居节点加入队列,并将其标记为已访问。 - 如果队列为空且未找到目标节点,则搜索失败,返回空结果。
问题实践
以一个4*4的迷宫为例,其中0表示可走,1表示障碍物,起点为(0,0),即左上角,终点为(3,3),即右下角。迷宫可表示为:
0 1 0 0
0 0 0 1
1 0 1 1
0 0 0 0
运行结果如下:
find the path
(0, 0) -> (1, 0) -> (1, 1) -> (2, 1) -> (3, 1) -> (3, 2) -> (3, 3)
Path length: 6 steps
算法实现
find the path
完整路径: [(0, 0), (1, 0), (1, 1), (2, 1), (3, 1), (3, 2), (3, 3)]
Path length: 6 steps
执行分析
| 步骤 | 第0层 | 第1层 | 第2层 | 第3层 | 第4层 | 第5层 | 第6层 | 第7层 | 第7层 | 第7层 | 第7层 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 初始队列 | (0,0) | (1,0) | (1,1) | (1,2), (2,1) | (2,1),(0,2) | (0,2),(3,1) | (3,1),(0,3) | (0,3),(3,0),(3,2) | (3,0),(3,2) | (3,2) | (3,3) |
| 弹出队首节点 | (0,0) | (1,0) | (1,1) | (1,2) | (2,1) | (0,2) | (3,1) | (0,3) | (3,0) | (3.2) | 找到终点了! |
| 新加入队列的节点 | (1,0) | (1,1) | (1,2), (2,1) | (0,2) | (3,1) | (0,3) | (3,0),(3,2) | None | None | (3,3) | 找到终点了! |
| 队列现状 | (1,0) | (1,1) | (1,2), (2,1) | (2,1),(0,2) | (0,2),(3,1) | (3,1),(0,3) | (0,3),(3,0),(3,2) | (3,0),(3,2) | (3,2) | (3,3) | 找到终点了! |
代码编写
#使用BFS去遍历一个无向图
import collectionsclass Node:def __init__(self, x,y,parent=None,depth=0):self.x = xself.y = yself.parent = parentself.depth = depthdef BFS(maze, node_start, node_end):visited = [[0 for col in range(len(maze))] for row in range(len(maze[0]))]path = collections.deque() #创建一个双端队列,用来存储待访问的节点visited[node_start.x][ node_start.y] = True #将起始节点标记为已访问path.append(node_start) #将起始节点放入队列 while path: #当队列不为空时:node_current = path.popleft()current_pos = (node_current.x, node_current.y)if node_current.x == node_end.x and node_current.y == node_end.y: #如果当前节点是目标节点,则回溯产生路径print("find the path")complete_path=[]while node_current:complete_path.append((node_current.x,node_current.y))node_current = node_current.parentcomplete_path.reverse()print("完整路径:", complete_path)print(f"Path length: {len(complete_path) - 1} steps")return 1for i in range(4) : #遍历当前节点的邻居节点next_x = node_current.x + directions[i][0]next_y = node_current.y + directions[i][1]if next_x >= 0 and next_x < len(maze) and next_y >= 0 and next_y < len(maze[0]) and maze[next_x][next_y] == 0 and visited[next_x][next_y] == False: #如果邻居节点在地图内,且不是障碍物,且未被访问过visited[next_x][next_y] = Truenode_next = Node(next_x,next_y,node_current,node_current.depth+1) #创建一个新的节点,并设置其父节点为当前节点,深度为当前节点的深度加1path.append(node_next) #将新节点放入队列if __name__ == "__main__":maze = [[0 for _ in range(4)] for _ in range(4)]maze[0] = [0,1,0,0]maze[1] = [0,0,0,1]maze[2] = [1,0,1,1]maze[3] = [0,0,0,0]directions = [[0,-1],[-1,0],[0,1],[1,0]] #左,上,右,下# 设置起点和终点(坐标从0开始)start = Node(0, 0)end = Node(3, 3)# 执行DFS搜索BFS(maze,start, end)
问题思考
从直观意义上来看:“DFS 比 BFS 的时间复杂度高,是因为 DFS 每当走到死路时会回溯,回溯的时间即为多出来的时间复杂度”
然而,从形式上的时间复杂度分析,假设图(或迷宫)有:
- 顶点数:V
- 边数:E
那么:
- DFS 的时间复杂度:O(V + E)
- BFS 的时间复杂度:O(V + E)
也就是说,从渐进复杂度角度,DFS 和 BFS 的理论时间复杂度其实是一样的。
如果我现在有一个走迷宫的问题,我应该如何选择 BFS 和 DFS 呢?
如果真让“一个人”在迷宫中走:
-
DFS: 这个人会“探索—回头—再探索”,反复走回头路。
-
BFS: 如果你真能派“一群人”同步探索(每个路口一个人),则能最快发现出口,但人力开销大。
However,如果限制为“只有一个人”,那他不可能同时分身成 BFS 的效果,他只能顺序地去尝试不同路径。也就是说“DFS 行走=(单人探索 + 回溯)”,“BFS 行走 =(多层同时扩展)”
所以,可以认为:
“一个人执行 BFS 策略的总时间 ≈ 执行 DFS 的总时间量级”