3. 图搜索基础:图论概念、DFS与BFS原理及代码实现
说实话,做机器人路径规划这些年,我最大的体会就是:图搜索是绕不开的坎。不管你后面用A*、Dijkstra还是RRT,底层逻辑都离不开图。今天咱们就把这个地基打牢。
3.1 图论基础:先搞清楚“图”长什么样
图是什么?说白了就是一堆节点和一堆边。节点代表位置,边代表能走的路。我当年刚入行时,总觉得图论是数学课的事,直到第一次写路径规划代码,才发现——嗯,真香。
3.1.1 图的分类
| 类型 | 特点 | 举个栗子 |
|---|---|---|
| 无向图 | 边没有方向,A↔B | 双向道路 |
| 有向图 | 边有方向,A→B | 单行道 |
| 加权图 | 边带权重(距离/时间) | 高速 vs 小路 |
| 无权图 | 所有边权重相同 | 迷宫格子 |
核心概念速记:
- 顶点(Vertex):图中的点,比如路口
- 边(Edge):连接顶点的线,比如道路
- 路径(Path):从A到B经过的顶点序列
- 环(Cycle):起点=终点的路径
- 连通图:任意两点都有路径可达
3.1.2 图的存储方式
我习惯用两种方式存图,看场景选:
方式一:邻接矩阵
# 用二维数组存,简单粗暴
graph = [
[0, 1, 0, 1], # 节点0连接1和3
[1, 0, 1, 0], # 节点1连接0和2
[0, 1, 0, 1], # 节点2连接1和3
[1, 0, 1, 0] # 节点3连接0和2
]
方式二:邻接表
# 用字典存,省空间
graph = {
0: [1, 3],
1: [0, 2],
2: [1, 3],
3: [0, 2]
}
我的经验:节点少(<100)用邻接矩阵,查边快;节点多(>1000)用邻接表,省内存。有一次我处理一个2000节点的地图,用邻接矩阵直接爆内存了……从那以后我学乖了。
3.2 深度优先搜索(DFS):一条路走到黑
DFS的思路很简单:从起点出发,沿着一条路走到头,走不动了就回头,换条路继续走。你想想看,就像你在迷宫里,左手扶墙一直走,总能走出去——但可能绕远路。
3.2.1 DFS原理
- 用栈(Stack)实现,后进先出
- 递归或迭代两种写法
- 适合判断连通性、拓扑排序、找所有路径
注意:DFS不保证找到最短路径!它只保证能找到一条路径(如果存在的话)。我曾经有个同事用DFS做最短路径规划,结果机器人绕了半个地图……
3.2.2 DFS代码实现(Python)
def dfs(graph, start, visited=None):
if visited is None:
visited = set()
visited.add(start)
print(f"访问节点: {start}")
for neighbor in graph[start]:
if neighbor not in visited:
dfs(graph, neighbor, visited)
return visited
# 测试
graph = {
'A': ['B', 'C'],
'B': ['A', 'D', 'E'],
'C': ['A', 'F'],
'D': ['B'],
'E': ['B', 'F'],
'F': ['C', 'E']
}
print("DFS遍历顺序:")
dfs(graph, 'A')
输出:
DFS遍历顺序:
访问节点: A
访问节点: B
访问节点: D
访问节点: E
访问节点: F
访问节点: C
3.3 广度优先搜索(BFS):层层推进
BFS的思路刚好相反:从起点开始,一层一层往外扩。就像往水里扔石头,波纹一圈圈扩散。我第一次用BFS做路径规划时,发现它天然适合找最短路径——因为先碰到的就是最近的。
3.3.1 BFS原理
- 用队列(Queue)实现,先进先出
- 按距离起点远近逐层访问
- 适合最短路径、层级遍历
关键区别:
- DFS用栈 → 深度优先,一条路走到黑
- BFS用队列 → 广度优先,层层推进
- DFS适合探索所有可能性
- BFS适合找最近目标
3.3.2 BFS代码实现(Python)
from collections import deque
def bfs(graph, start):
visited = set()
queue = deque([start])
visited.add(start)
while queue:
node = queue.popleft()
print(f"访问节点: {node}")
for neighbor in graph[node]:
if neighbor not in visited:
visited.add(neighbor)
queue.append(neighbor)
return visited
# 测试
print("\nBFS遍历顺序:")
bfs(graph, 'A')
输出:
BFS遍历顺序:
访问节点: A
访问节点: B
访问节点: C
访问节点: D
访问节点: E
访问节点: F
3.4 DFS vs BFS:到底选哪个?
| 对比项 | DFS | BFS |
|---|---|---|
| 数据结构 | 栈(Stack) | 队列(Queue) |
| 空间复杂度 | O(h),h为深度 | O(w),w为宽度 |
| 最短路径 | 不保证 | 保证(无权图) |
| 适用场景 | 拓扑排序、连通性 | 最短路径、层级遍历 |
| 实现难度 | 递归简单,迭代稍复杂 | 队列实现,逻辑清晰 |
避坑指南:我曾经在项目中用DFS做迷宫求解,结果因为递归太深导致栈溢出。后来改成BFS,不仅没溢出,还找到了最短路径。所以——如果图很深但很窄,用DFS;如果图很宽但很浅,用BFS。
3.5 知识体系总览
下面这张图是我自己画的,把本章的核心逻辑串起来了:
3.6 实战小贴士
最后分享几个我踩过的坑:
- 别忘了标记已访问节点——不然会死循环。我刚开始写DFS时忘了这步,程序跑了一下午没停……
- 递归深度限制——Python默认递归深度1000,图大了会报错。可以用
sys.setrecursionlimit()调大,或者直接写迭代版本。 - BFS的队列别用list——用
collections.deque,否则pop(0)是O(n)操作,大数据量时慢得你想哭。 - 加权图别用BFS——BFS只保证无权图的最短路径。加权图请用Dijkstra或A*,后面章节会讲。
一句话总结:DFS适合探索,BFS适合找近路。两者都是图搜索的基石,后面的A*、Dijkstra都是在这两个基础上加启发式信息。把今天的内容吃透,后面你会学得飞快。