第一章 路径规划基础:图搜索算法

大家好,我是你们的老朋友。今天咱们聊聊路径规划里最基础的东西——图搜索算法。

说实话,我刚入行那会儿,觉得路径规划不就是找条路嘛,有啥难的?后来真上了车,才发现事情没那么简单。你想想看,一个城市道路网,节点成千上万,怎么在几十毫秒内找到最优路径?这就是图搜索算法要解决的问题。

1.1 图的基本概念

先说说图是什么。图由节点和边组成。节点代表位置,边代表连接关系。比如十字路口是节点,路段是边。

图分两种:有向图和无向图。有向图里,边有方向,比如单行道。无向图里,边没方向,比如双向车道。

还有一种叫加权图。每条边带一个权重,代表距离、时间或代价。咱们做路径规划,基本都在加权图上干活。

核心要点:路径规划的本质,就是在加权图上找到从起点到终点的最优路径。

1.2 BFS与DFS:最朴素的搜索

BFS(广度优先搜索)和DFS(深度优先搜索)是最基础的两种搜索策略。

BFS:一层一层往外扩。像水波一样,从起点开始,先搜所有距离为1的节点,再搜距离为2的,以此类推。

DFS:一条路走到黑。从起点出发,沿着一条路径一直往下走,走不通了再回头。

我刚开始学的时候,总觉得DFS更聪明,因为它能快速找到一条路径。但后来发现,BFS在路径规划里更实用。为什么?因为BFS找到的第一条路径就是最短路径(在无权图上)。

个人经验:我在做停车场路径规划时,用过BFS。停车场内部道路简单,节点不多,BFS跑起来又快又稳。但上了城市道路,节点一多,BFS就扛不住了。

代码实现其实不复杂:

// BFS伪代码
function BFS(start, goal):
    queue = [start]
    visited = {start}
    parent = {}
    
    while queue is not empty:
        current = queue.pop(0)
        if current == goal:
            return reconstruct_path(parent, start, goal)
        
        for neighbor in current.neighbors:
            if neighbor not in visited:
                visited.add(neighbor)
                parent[neighbor] = current
                queue.append(neighbor)
    
    return null  // 没找到路径

1.3 Dijkstra算法:带权重的BFS

BFS只能处理无权图。但现实世界里的路,每条都有长度、时间、拥堵程度。这时候就需要Dijkstra算法了。

Dijkstra算法的核心思想:维护一个距离表,记录从起点到每个节点的最短距离。每次选距离最小的节点展开,更新邻居的距离。

说白了,Dijkstra就是BFS的升级版。BFS用队列,Dijkstra用优先队列(最小堆)。

注意:Dijkstra算法要求边的权重不能为负。如果路网里有负权边,得用Bellman-Ford算法。不过咱们做自动驾驶,路网权重都是正数,所以Dijkstra够用了。

代码实现:

// Dijkstra伪代码
function Dijkstra(start, goal):
    dist = {start: 0}
    parent = {}
    pq = priority_queue()
    pq.push((0, start))
    
    while pq is not empty:
        current_dist, current = pq.pop()
        
        if current == goal:
            return reconstruct_path(parent, start, goal)
        
        if current_dist > dist[current]:
            continue
        
        for neighbor, weight in current.neighbors:
            new_dist = current_dist + weight
            if new_dist < dist.get(neighbor, INF):
                dist[neighbor] = new_dist
                parent[neighbor] = current
                pq.push((new_dist, neighbor))
    
    return null

我曾经在一个项目中,用Dijkstra做全局路径规划。路网有5000多个节点,跑一次大概要200毫秒。对于全局规划来说,这个速度还行。但要是做局部规划,每100毫秒就要更新一次路径,Dijkstra就有点吃力了。

1.4 A*算法:启发式搜索

A*算法是Dijkstra的改进版。它引入了一个启发式函数,告诉算法「往哪个方向走更有可能找到终点」。

A*的代价函数:f(n) = g(n) + h(n)

  • g(n):从起点到节点n的实际代价
  • h(n):从节点n到终点的估计代价(启发式函数)

启发式函数怎么设计?最常用的是欧几里得距离和曼哈顿距离。

启发式函数 适用场景 特点
欧几里得距离 自由空间(如空地、空中) 直线距离,计算简单
曼哈顿距离 网格地图(如城市道路) 只允许上下左右移动
对角线距离 允许对角线移动的网格 比曼哈顿更精确

关键点:启发式函数必须满足「可采纳性」,即估计代价不能超过实际代价。否则A*可能找不到最优解。

代码实现:

// A*伪代码
function AStar(start, goal):
    open_set = {start}
    closed_set = {}
    g = {start: 0}
    f = {start: heuristic(start, goal)}
    parent = {}
    
    while open_set is not empty:
        current = node in open_set with lowest f
        
        if current == goal:
            return reconstruct_path(parent, start, goal)
        
        open_set.remove(current)
        closed_set.add(current)
        
        for neighbor in current.neighbors:
            if neighbor in closed_set:
                continue
            
            tentative_g = g[current] + distance(current, neighbor)
            
            if neighbor not in open_set:
                open_set.add(neighbor)
            elif tentative_g >= g[neighbor]:
                continue
            
            parent[neighbor] = current
            g[neighbor] = tentative_g
            f[neighbor] = g[neighbor] + heuristic(neighbor, goal)
    
    return null

1.5 三种算法的对比

算法 适用场景 时间复杂度 空间复杂度 是否保证最优
BFS 无权图,小规模 O(V+E) O(V) 是(无权图)
Dijkstra 加权图,中等规模 O((V+E)logV) O(V)
A* 加权图,大规模 取决于启发式函数 O(V) 是(可采纳启发式)

我的建议:实际项目中,我一般这样选:

  • 小地图(几百个节点):BFS或Dijkstra都行
  • 中等地图(几千个节点):Dijkstra
  • 大地图(上万个节点):A*,配合好的启发式函数

1.6 工程实现中的坑

嗯,这里要注意几个容易踩坑的地方。

第一个坑:启发式函数设计不当。我曾经用曼哈顿距离做城市道路的启发式,结果A*跑得比Dijkstra还慢。后来发现,城市道路有斜街,曼哈顿距离低估了实际距离,导致搜索范围过大。

第二个坑:优先队列的实现。用二叉堆没问题,但要注意重复节点的处理。我见过有人把同一个节点多次入队,导致内存爆炸。

第三个坑:实时性要求。自动驾驶里,路径规划必须在几十毫秒内完成。如果地图太大,可以考虑分层规划——先粗粒度规划,再细粒度优化。

避坑指南:我曾经在一个项目中,用A*做全局路径规划,地图有10万个节点。跑一次要3秒多,根本没法用。后来改成分层规划,先在高层次规划主干道,再在低层次规划细节路径,时间降到了50毫秒以内。

1.7 小结

这一章我们聊了图搜索的基础。BFS是最朴素的,Dijkstra是带权重的BFS,A*是带方向的Dijkstra。

说白了,这三兄弟一个比一个聪明,但一个比一个复杂。实际项目中,要根据场景选择合适的算法。

下一章,咱们聊聊更高级的路径规划算法——RRT和PRM。这些算法能处理高维空间和复杂约束,是自动驾驶里常用的技术。

记住,算法是工具,不是目的。理解每个算法的优缺点,才能在工程中做出正确的选择。

公众号:蓝海资料掘金营,微信deep3321