3. 图搜索基础:图论基本概念与DFS/BFS

各位同学好,我是你们这节《路径规划与决策算法入门》的讲师。今天我们来聊聊图搜索。说实话,图搜索是整个路径规划算法的地基。你后面学的A*、Dijkstra、RRT,本质上都是在图搜索这个框架上做文章。所以这一节,咱们把地基打牢。

3.1 图论基本概念:节点、边、权重

先问个问题:什么是图?

说白了,图就是一堆「点」和连接这些点的「线」。在路径规划里,点就是机器人可能到达的位置,线就是位置之间的可行路径。

节点(Node/Vertex):代表状态或位置。比如一个房间的角落、一个路口、一个栅格的中心点。

边(Edge):代表两个节点之间的连接关系。如果两个节点之间有边,说明机器人可以直接从A走到B。

权重(Weight):边的代价。可以是距离、时间、能耗,甚至风险值。我做过一个仓储机器人项目,权重就是「路径长度 + 障碍物密度惩罚项」。嗯,这个惩罚项帮我们避开了很多拥堵区域。

核心概念速查表

术语含义路径规划中的例子
节点状态/位置栅格地图中的每个格子
边连接关系相邻格子之间的可通行路径
权重代价移动距离、时间、能耗
有向图边有方向单行道、下坡路
无向图边无方向双向通行的走廊

我个人习惯把图分为两种:有向图和无向图。无向图就是A能到B,B也能到A。有向图则像单行道,只能一个方向走。你想想看,在真实机器人场景里,斜坡、电梯、单向门,这些都得用有向图来表示。

3.2 深度优先搜索(DFS)

DFS,说白了就是「一条路走到黑,撞了南墙再回头」。它的核心思想是:从起点出发,沿着一条分支一直往下探索,直到走不动了,再回溯到上一个分叉点,换条路继续走。

DFS的算法流程:

  1. 把起点压入栈(Stack)
  2. 从栈顶弹出一个节点,标记为已访问
  3. 检查该节点是否为目标节点。如果是,结束
  4. 否则,把所有未访问的邻居节点压入栈
  5. 重复步骤2-4,直到栈为空

这里的关键数据结构是栈。后进先出(LIFO)的特性,决定了DFS会优先深入探索,而不是横向扩展。

避坑指南:我曾经在一个迷宫导航项目里用DFS,结果机器人绕了远路。为什么?因为DFS不保证找到最短路径。它只保证「能找到一条路径」。如果你需要最短路径,请用BFS或Dijkstra。

# DFS 伪代码
def dfs(graph, start, goal):
    stack = [start]
    visited = set()
    
    while stack:
        node = stack.pop()  # 从栈顶取
        if node == goal:
            return True
        if node not in visited:
            visited.add(node)
            # 把未访问的邻居压入栈
            for neighbor in graph[node]:
                if neighbor not in visited:
                    stack.append(neighbor)
    return False

DFS的时间复杂度是O(V+E),V是节点数,E是边数。空间复杂度最坏情况O(V)。嗯,这里要注意,如果图特别深,递归实现的DFS可能会栈溢出。我建议用迭代版本,更可控。

3.3 广度优先搜索(BFS)

BFS和DFS正好相反。它是「层层推进,地毯式搜索」。从起点开始,先探索所有距离为1的节点,再探索距离为2的节点,以此类推。

BFS的算法流程:

  1. 把起点放入队列(Queue)
  2. 从队列头部取出一个节点,标记为已访问
  3. 检查是否为目标节点
  4. 把所有未访问的邻居节点加入队列尾部
  5. 重复步骤2-4,直到队列为空

关键数据结构是队列。先进先出(FIFO)保证了「先发现的节点先被扩展」,这就是BFS能找到最短路径的根本原因。

注意:BFS能找到最短路径,前提是所有边的权重相同。如果边的权重不同(比如有的路长、有的路短),BFS就不灵了。这时候得用Dijkstra算法。我在做AGV调度时吃过这个亏,后来老老实实换了Dijkstra。

# BFS 伪代码
def bfs(graph, start, goal):
    queue = [start]
    visited = set()
    
    while queue:
        node = queue.pop(0)  # 从队列头部取
        if node == goal:
            return True
        if node not in visited:
            visited.add(node)
            for neighbor in graph[node]:
                if neighbor not in visited:
                    queue.append(neighbor)  # 加入队列尾部
    return False

BFS的时间复杂度也是O(V+E)。但空间复杂度比DFS高,因为队列里可能同时存很多节点。尤其是当图的分支因子很大时,BFS的内存消耗会暴涨。

3.4 DFS vs BFS:怎么选?

我经常被问到:「到底用DFS还是BFS?」

我的回答是:看需求。

场景推荐算法原因
需要最短路径(等权图)BFSBFS保证找到最短路径
内存有限DFSDFS空间复杂度通常更低
解可能很深DFSDFS能快速深入,BFS可能爆炸
解可能很浅BFSBFS在浅层就能找到解
需要遍历所有节点两者均可都能遍历,但DFS实现更简洁

举个例子。我在做扫地机器人全覆盖路径规划时,用的是DFS。因为需要遍历所有房间,DFS的递归回溯特性天然适合「走遍每个角落」。但如果是送餐机器人找最短路径,我肯定用BFS。

3.5 知识体系总览

下面这张图是我手绘的本章知识结构,帮你理清脉络。

图搜索基础:知识体系 图论基本概念 节点、边、权重 有向图 vs 无向图 深度优先搜索 (DFS) 栈 (LIFO) 一条路走到黑 不保证最短路径 广度优先搜索 (BFS) 队列 (FIFO) 层层推进 保证最短路径(等权) 选择建议 需要最短路径 → BFS | 内存有限/解很深 → DFS

3.6 实战小贴士

最后分享几个我在项目中积累的经验:

  • 别用递归DFS处理大图。Python默认递归深度只有1000左右,超过就报错。用栈实现迭代版本更安全。
  • BFS的队列别用list.pop(0)。这个操作是O(n)的,数据量大时巨慢。用collections.deque,popleft是O(1)。
  • 记得标记已访问节点。不然DFS/BFS都会陷入死循环。我见过有人忘了这步,程序跑了半小时没停。
  • 权重不同时,BFS不保证最优。这是新手最容易犯的错误。等权图用BFS,不等权用Dijkstra。

好了,图搜索基础就讲到这里。记住,DFS和BFS是路径规划的两把入门钥匙。后面学A*、Dijkstra时,你会发现它们都是在BFS的基础上加了「启发式」或「权重」的改进。地基打牢了,后面盖楼才稳。

可可精品资料库