4. 路径规划基础:图搜索算法与采样方法

大家好,我是你们的老朋友。今天我们来聊聊路径规划里最基础、也最核心的一块——图搜索和采样方法。说实话,我刚入行那会儿,觉得路径规划不就是找条路嘛,有啥难的?直到我在一个仓储机器人项目里,看着机器人在货架间来回撞墙,我才意识到:选对算法,比写对代码更重要。

这一章,我会把Dijkstra、A*、RRT、RRT*和PRM这五个算法掰开揉碎讲清楚。嗯,咱们不搞虚的,直接上干货。

4.1 图搜索算法:Dijkstra与A*

先说说图搜索。你想想看,机器人要在地图里找一条从起点到终点的路,本质上就是在图里找最短路径。图是什么?说白了就是一堆节点和连接它们的边。

4.1.1 Dijkstra算法:稳扎稳打的“老黄牛”

Dijkstra算法,我习惯叫它“地毯式搜索”。它从起点开始,一层一层往外扩,直到找到终点。它的核心思想就一句话:每次选当前代价最小的节点扩展。

核心公式:g(n) = 从起点到节点n的实际代价

算法永远选择g(n)最小的节点去探索。

我在一个AGV调度项目里用过Dijkstra。当时地图不大,节点也就几百个,跑起来没问题。但后来地图扩大到几千个节点,那速度就有点捉急了。为什么?因为它太“老实”了,不管终点在哪个方向,它都一视同仁地往外扩。

// Dijkstra伪代码
function Dijkstra(graph, start, goal):
    openSet = {start}
    g[start] = 0
    
    while openSet is not empty:
        current = node in openSet with smallest g
        if current == goal:
            return reconstructPath()
        
        openSet.remove(current)
        for each neighbor of current:
            tentative_g = g[current] + cost(current, neighbor)
            if tentative_g < g[neighbor]:
                g[neighbor] = tentative_g
                parent[neighbor] = current
                if neighbor not in openSet:
                    openSet.add(neighbor)
    return failure

我的经验:Dijkstra适合地图不大、或者需要保证最优解的场合。如果你对实时性要求高,建议换A*。

4.1.2 A*算法:带“指南针”的搜索

A*算法,说白了就是在Dijkstra的基础上加了个“指南针”。这个指南针就是启发式函数h(n),它估算从节点n到终点的距离。

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

其中g(n)是实际代价,h(n)是启发式估计代价。

我个人习惯用欧几里得距离作为启发式函数。但要注意,h(n)必须满足可采纳性——它不能高估实际代价。否则A*就找不到最优解了。

我曾经在一个无人机路径规划项目里用A*。当时地图是三维的,节点数上万。用Dijkstra要算好几秒,换成A*后,0.5秒就出结果了。差距就是这么明显。

// A*伪代码
function AStar(graph, start, goal):
    openSet = {start}
    g[start] = 0
    f[start] = h(start, goal)
    
    while openSet is not empty:
        current = node in openSet with smallest f
        if current == goal:
            return reconstructPath()
        
        openSet.remove(current)
        for each neighbor of current:
            tentative_g = g[current] + cost(current, neighbor)
            if tentative_g < g[neighbor]:
                g[neighbor] = tentative_g
                f[neighbor] = g[neighbor] + h(neighbor, goal)
                parent[neighbor] = current
                if neighbor not in openSet:
                    openSet.add(neighbor)
    return failure
算法 优点 缺点 适用场景
Dijkstra 保证最优解,实现简单 搜索范围大,速度慢 小地图、需要最优解
A* 搜索效率高,有方向性 依赖启发式函数设计 中大型地图、实时性要求高

避坑指南:我曾经在A*里用了曼哈顿距离做启发式,结果在开阔地图上效率反而比Dijkstra还差。后来才发现,曼哈顿距离不适合对角线移动的场景。记住:启发式函数要和实际运动模型匹配。

4.2 采样方法:RRT、RRT*与PRM

图搜索算法有个硬伤——它需要把地图离散化成网格或节点。如果地图很大,或者维度很高(比如机械臂的关节空间),那节点数会爆炸。这时候,采样方法就派上用场了。

4.2.1 RRT算法:快速探索随机树

RRT,全称Rapidly-exploring Random Tree。说白了就是在地图里随机撒点,然后从起点开始,像树一样往外长。每次随机采样一个点,然后从最近的已有节点向它延伸一步。

我记得第一次用RRT是在一个狭窄通道的路径规划里。Dijkstra和A*都因为网格分辨率的问题找不到路,但RRT轻松就搞定了。为什么?因为它不需要离散化地图,直接在连续空间里采样。

// RRT伪代码
function RRT(start, goal, maxIterations):
    tree = {start}
    for i = 1 to maxIterations:
        randomPoint = sampleRandom()
        nearestNode = findNearest(tree, randomPoint)
        newNode = extend(nearestNode, randomPoint, stepSize)
        if collisionFree(nearestNode, newNode):
            tree.add(newNode)
            if distance(newNode, goal) < threshold:
                return reconstructPath(newNode)
    return failure

我的建议:RRT的步长很关键。步长太大,容易穿过障碍物;步长太小,树长得太慢。我一般设成地图对角线长度的5%-10%。

4.2.2 RRT*算法:渐近最优的RRT

RRT有个问题——它找到的路径通常不是最优的。RRT*就是在RRT的基础上加了两个优化步骤:重选父节点和重布线。

说白了,RRT*在每次添加新节点后,会检查附近有没有更好的父节点。如果有,就重新连接。这样随着采样点增多,路径会逐渐逼近最优解。

我在一个自动驾驶项目的局部路径规划里用过RRT*。虽然它比RRT慢一些,但路径平滑多了,车辆开起来不会忽左忽右。

// RRT*伪代码(关键部分)
function RRTStar(start, goal, maxIterations):
    tree = {start}
    for i = 1 to maxIterations:
        randomPoint = sampleRandom()
        nearestNode = findNearest(tree, randomPoint)
        newNode = extend(nearestNode, randomPoint, stepSize)
        if collisionFree(nearestNode, newNode):
            // 重选父节点
            nearNodes = findNear(tree, newNode, radius)
            for each node in nearNodes:
                if collisionFree(node, newNode):
                    if g[node] + cost(node, newNode) < g[newNode]:
                        parent[newNode] = node
                        g[newNode] = g[node] + cost(node, newNode)
            
            tree.add(newNode)
            
            // 重布线
            for each node in nearNodes:
                if collisionFree(newNode, node):
                    if g[newNode] + cost(newNode, node) < g[node]:
                        parent[node] = newNode
                        g[node] = g[newNode] + cost(newNode, node)
    return failure

4.2.3 PRM算法:概率路线图

PRM和RRT的思路不太一样。PRM是先建图,再查路。它在地图里随机采样大量点,然后连接相邻的、无碰撞的点,形成一个路线图。之后要查路径时,就在这个图上用Dijkstra或A*找路。

PRM特别适合多次查询的场景。比如一个工厂里的多个机器人,共享同一个地图。你只需要建一次PRM图,所有机器人都可以用。

PRM vs RRT:PRM适合多查询场景,RRT适合单查询场景。PRM的图是全局的,RRT的树是单次查询的。

我曾经在一个仓库调度系统里用PRM。地图有5000多平方米,我们提前建好PRM图,然后每个AGV要导航时,直接在图里搜就行。响应时间从秒级降到了毫秒级。

// PRM伪代码
function PRM(numSamples, k):
    // 建图阶段
    graph = empty
    for i = 1 to numSamples:
        point = sampleFree()
        graph.addNode(point)
    
    for each node in graph:
        neighbors = findKNearest(node, k)
        for each neighbor in neighbors:
            if collisionFree(node, neighbor):
                graph.addEdge(node, neighbor)
    
    // 查询阶段
    function query(start, goal):
        connect start and goal to graph
        path = Dijkstra(graph, start, goal)
        return path

4.3 知识体系总览

说了这么多,我画了一张图来总结这五个算法的关系。你一看就明白了。

路径规划算法分类 路径规划 图搜索算法 采样方法 Dijkstra A* RRT RRT* PRM 保证最优解 搜索范围大 带启发式 效率高 快速探索 非最优 渐近最优 重选父节点 先建图,再查路 适合多查询

从这张图你可以看到,路径规划算法大致分两类:图搜索和采样方法。图搜索里,Dijkstra是基础,A*是它的升级版。采样方法里,RRT是基础,RRT*是它的优化版,而PRM走的是另一条路——先建图再查路。

选型建议:

  • 地图小、要最优解 → Dijkstra
  • 地图中等、要效率 → A*
  • 高维空间、单次查询 → RRT
  • 高维空间、要最优 → RRT*
  • 多机器人、多查询 → PRM

好了,这一章的内容就到这里。这些算法都是路径规划的基石,我建议你动手实现一遍,光看是学不会的。嗯,我在做第一个RRT实现时,光调试碰撞检测就花了两天——但搞明白之后,后面的路就顺了。

可可精品资料库