路径规划算法学习Day2:广度优先搜索算法(BFS)
前言
如果我想要用一群人来走迷宫,人的总数确定,从一点出发,每到一个节点就分出去一个人,那么我就可以根据要探索的层的数量来判断实际所需要的人数,应该是呈现一个2为底数的幂指数关系,进一步来说,假如迷宫终点是一个宝藏,当其中一个人找到了宝藏,迷宫立刻出现一个随机的变化成为结构不同的新迷宫,由于现在的人已经分散到了迷宫的不同位置,似乎就没办法遵循一群人走迷宫的策略了,为了能够在新的迷宫中找到宝藏,要采取怎样的策略,显然仅仅采用BFS是难以解决的
然而,BFS对于迷宫问题仍有其优势,我们知道,最简单的走迷宫的方法就是遵照右手定则,那么如果有这样一个迷宫,即下面的矩阵所示:人面向y轴方向,矩阵的四周为墙壁,左下角为起点(0,0),终点用“*”来标注,不难得出,如果按照(右,上,左,下)的遍历顺序,将导致人永远在迷宫的最外层而无法到达终点
在笔者看来,只需要添加一个集合,用其存储已经访问过的节点,这也是DFS和BFS的一个共同点,这样就可以避免人绕圈子,从而找到终点。DFS在上一篇博客中已经介绍过了,这里就进行BFS的学习,如果笔者的理解产生错误,欢迎各位读者老爷批评指正!
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 1 1 1 1 0 0 0
0 0 0 1 0 * 1 0 0 0
0 0 0 1 0 1 1 0 0 0
0 0 0 1 0 0 0 0 0 0 y
0 0 0 0 0 0 0 0 0 0 ^
0 0 0 0 0 0 0 0 0 0 |
0 0 0 0 0 0 0 0 0 0 |______>x
算法学习(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 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 初始队列 | (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 collections
class Node:
def __init__(self, x,y,parent=None,depth=0):
self.x = x
self.y = y
self.parent = parent
self.depth = depth
def 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.parent
complete_path.reverse()
print("完整路径:", complete_path)
print(f"Path length: {len(complete_path) - 1} steps")
return 1
for 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] = True
node_next = Node(next_x,next_y,node_current,node_current.depth+1) #创建一个新的节点,并设置其父节点为当前节点,深度为当前节点的深度加1
path.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 的总时间量级”

浙公网安备 33010602011771号