第三章 路径搜索基础:图搜索算法概览、Dijkstra算法原理与实现、A*算法原理与实现

各位同学,欢迎来到路径搜索的世界。说实话,这一章是整个路径规划课程的基石。你想想看,无论是无人车在十字路口选择车道,还是机器人绕过障碍物,底层都离不开这些图搜索算法。我个人习惯把这一章叫做「找路的三板斧」——图搜索基础、Dijkstra、A*。今天咱们就把它彻底吃透。

3.1 图搜索算法概览:从地图到图

先问个问题:为什么叫「图搜索」?

因为现实世界的地图,在计算机眼里就是一张图(Graph)。节点代表位置,边代表可通行的路径。我刚开始做自动驾驶时,总想着直接处理像素级的地图,后来发现那太笨了。把地图抽象成图,搜索效率能提升几个数量级。

图搜索的核心就三件事:

  • 节点(Node):比如路口、车道中心点、栅格中心
  • 边(Edge):节点之间的连接,通常带有权重(距离、时间、能耗)
  • 搜索策略:怎么从起点走到终点,且代价最小

常见的图搜索算法可以分为两类:

类别 代表算法 特点
盲目搜索 BFS、DFS、Dijkstra 不利用目标信息,遍历所有可能
启发式搜索 A*、D*、RRT 利用启发函数引导搜索方向

嗯,这里要注意:盲目搜索不代表「笨」,它保证能找到最优解,只是慢。启发式搜索快,但需要好的启发函数。我在项目中遇到过,选错算法导致路径规划超时,车辆直接停在路中间——那场面,尴尬得很。

3.2 Dijkstra算法原理:稳扎稳打的贪心

Dijkstra算法,说白了就是「从起点出发,一层一层往外扩,每次选当前代价最小的节点」。它保证找到的是最短路径,前提是边的权重非负。

核心思想就一句话:维护一个距离表,每次从未访问的节点中选距离最小的,然后更新它的邻居

伪代码长这样:

function Dijkstra(Graph, start):
    dist[start] = 0
    for each node v in Graph:
        if v != start: dist[v] = INF
        visited[v] = False
    
    while 还有未访问的节点:
        u = 未访问节点中 dist 最小的
        visited[u] = True
        for each neighbor v of u:
            if not visited[v]:
                new_dist = dist[u] + weight(u, v)
                if new_dist < dist[v]:
                    dist[v] = new_dist
                    prev[v] = u
    return dist, prev

我曾经在高速场景下用过Dijkstra做全局路径规划。说实话,效果很稳,但就是慢。城市道路几百个节点还好,要是高精地图几万个节点,那计算时间能让你怀疑人生。

避坑指南: Dijkstra不能处理负权边。我曾经天真地以为「负权边代表下坡路,应该更快」,结果算法直接崩了。记住,负权边请用Bellman-Ford。

3.3 Dijkstra算法实现:手写一个最小堆版本

实际工程中,我们不会用数组找最小节点,那太慢了。用最小堆(优先队列)来优化,复杂度从O(V²)降到O((V+E)logV)。

Python实现如下:

import heapq

def dijkstra(graph, start):
    # graph: 邻接表 {node: [(neighbor, weight), ...]}
    dist = {node: float('inf') for node in graph}
    dist[start] = 0
    pq = [(0, start)]  # (距离, 节点)
    prev = {}
    
    while pq:
        current_dist, u = heapq.heappop(pq)
        if current_dist > dist[u]:
            continue  # 过时的记录,跳过
        for v, w in graph[u]:
            new_dist = dist[u] + w
            if new_dist < dist[v]:
                dist[v] = new_dist
                prev[v] = u
                heapq.heappush(pq, (new_dist, v))
    return dist, prev

注意看第8行那个continue判断。我刚开始写的时候没加,结果堆里塞满了旧数据,内存直接爆了。你想想看,一个节点被更新了十几次,堆里就有十几个记录,不跳过多浪费。

小技巧: 如果图是稀疏的(边数远小于节点数的平方),用邻接表+堆是最佳选择。稠密图反而可以用邻接矩阵+数组,因为堆的开销可能更大。

3.4 A*算法原理:带方向感的Dijkstra

A*算法,说白了就是Dijkstra加了一个「指南针」。这个指南针就是启发函数h(n),它估算从当前节点到终点的距离。

核心公式:f(n) = g(n) + h(n)

  • g(n):从起点到当前节点的实际代价(和Dijkstra一样)
  • h(n):从当前节点到终点的估计代价(启发函数)
  • f(n):总估计代价,优先选f最小的节点

为什么A*比Dijkstra快?因为h(n)引导搜索往目标方向走,而不是盲目地四周扩散。我做过对比实验:同样一张2000节点的地图,Dijkstra要遍历1800个节点,A*只遍历了300个——差距就是这么大。

但这里有个关键:h(n)必须满足可采纳性(admissible),即h(n) ≤ 真实代价。否则A*可能找不到最优解。我常用的启发函数:

场景 启发函数 说明
网格地图(四方向) 曼哈顿距离 |x1-x2| + |y1-y2|
网格地图(八方向) 切比雪夫距离 max(|x1-x2|, |y1-y2|)
任意方向 欧几里得距离 √((x1-x2)² + (y1-y2)²)
核心经验: 启发函数越接近真实代价,A*搜索越快。但千万别高估!高估了就不保证最优了。我一般取曼哈顿距离乘以1.0,最多1.01,再大就不敢了。

3.5 A*算法实现:从Dijkstra改两行代码

你猜怎么着?A*的代码和Dijkstra几乎一模一样,就改了两处:

  1. 优先队列里存的是f值,不是g值
  2. 需要计算启发函数h(n)

看代码:

import heapq

def heuristic(a, b):
    # 曼哈顿距离
    return abs(a[0] - b[0]) + abs(a[1] - b[1])

def a_star(graph, start, goal):
    # graph: 邻接表,节点是(x,y)坐标
    g = {node: float('inf') for node in graph}
    g[start] = 0
    f = {node: float('inf') for node in graph}
    f[start] = heuristic(start, goal)
    pq = [(f[start], start)]
    prev = {}
    
    while pq:
        current_f, u = heapq.heappop(pq)
        if u == goal:
            break
        if current_f > f[u]:
            continue
        for v, w in graph[u]:
            new_g = g[u] + w
            if new_g < g[v]:
                g[v] = new_g
                f[v] = new_g + heuristic(v, goal)
                prev[v] = u
                heapq.heappush(pq, (f[v], v))
    return g, prev

看到了吗?第17行计算f值,就是g值加上启发函数。其他逻辑和Dijkstra一模一样。所以我说,学会了Dijkstra,A*就是顺手的事。

注意: 如果启发函数h(n)=0,A*就退化成Dijkstra。所以Dijkstra其实是A*的一个特例。我在面试新人时经常问这个问题,能答上来的,说明真理解了。

3.6 实战对比:什么时候用哪个?

最后,我根据实际项目经验,给各位一个选择指南:

  • 需要绝对最优解,且图不大(<1000节点) → 用Dijkstra,简单可靠
  • 需要绝对最优解,图很大 → 用A*,但启发函数要精心设计
  • 需要快速找到可行解,不要求最优 → 用贪心最佳优先搜索(Greedy BFS),或者RRT
  • 动态环境,障碍物会变化 → 用D* Lite或A*重规划

我记得有一次做园区物流车,地图就500个节点,我偷懒用了A*。结果因为启发函数没调好,路径绕了个大弯。后来换成Dijkstra,虽然慢了0.2秒,但路径完美。所以啊,别盲目追求「高级」算法,适合场景的才是最好的。

好了,这一章的内容就到这。图搜索是路径规划的「内功」,Dijkstra和A*是两套最基础的「招式」。下一章咱们会讲更高级的算法,比如RRT和Hybrid A*,但今天这些基础打不牢,后面会越学越吃力。建议各位把代码亲手敲一遍,跑几个例子,感受一下搜索过程。