第四章 路径规划基础:图搜索与采样算法

各位同学好,我是你们的老朋友。今天咱们来聊聊路径规划——说白了,就是让机器人知道“怎么走”的问题。

我记得刚入行那会儿,带我的老师傅跟我说过一句话:“飞控搞得好不好,一半看路径规划。”当时我不太理解,后来踩了不少坑才明白——路径规划确实是整个系统的骨架。骨架歪了,后面调参再漂亮也白搭。

这一章,咱们重点啃四个算法:Dijkstra、A*、RRT 和 RRT*。外加一个路径平滑的技巧。嗯,内容不少,但我会尽量讲得接地气些。

路径规划基础 图搜索算法 Dijkstra A*(启发式) 采样算法 RRT RRT*(渐进最优) 路径平滑与优化 图搜索 → 离散空间 | 采样算法 → 连续空间

4.1 图搜索算法:Dijkstra 与 A*

先说说图搜索。你想想看,如果环境是已知的、离散的,比如一张栅格地图,那图搜索就是最直接的办法。

4.1.1 Dijkstra:最朴素的“地毯式搜索”

Dijkstra 算法的核心思想其实很简单:从起点开始,一层一层往外扩,每次都选当前代价最小的节点继续扩展。直到扩展到终点为止。

我在项目中遇到过一个问题:用 Dijkstra 做无人机航路规划,地图稍微大一点(比如 1000×1000 的栅格),计算时间就飙到好几秒。为什么?因为它没有方向感,四面八方都搜,很多无用功。

Dijkstra 的核心特点:

  • 保证找到最短路径(前提是边权非负)
  • 时间复杂度 O(V²) 或 O(E log V)(用优先队列)
  • 没有启发信息,搜索范围大
// 伪代码:Dijkstra 核心逻辑
function Dijkstra(graph, start, goal):
    openSet = {start}
    gScore[start] = 0
    
    while openSet is not empty:
        current = node in openSet with smallest gScore
        if current == goal:
            return reconstructPath(cameFrom, current)
        
        openSet.remove(current)
        for each neighbor of current:
            tentative_g = gScore[current] + dist(current, neighbor)
            if tentative_g < gScore[neighbor]:
                gScore[neighbor] = tentative_g
                cameFrom[neighbor] = current
                if neighbor not in openSet:
                    openSet.add(neighbor)
    return failure

我的小建议:如果地图不大(比如 200×200 以内),Dijkstra 完全够用。但要是地图大了,或者对实时性有要求,就得请出 A* 了。

4.1.2 A*:带“指南针”的 Dijkstra

A* 和 Dijkstra 的区别,说白了就一句话:Dijkstra 只知道“我已经走了多远”,A* 还知道“离终点大概还有多远”。

这个“大概还有多远”就是启发函数 h(n)。常用的有曼哈顿距离、欧几里得距离。我个人的习惯是,在栅格地图上用曼哈顿距离,在连续空间里用欧几里得距离。

// 伪代码:A* 核心逻辑
function AStar(graph, start, goal):
    openSet = {start}
    gScore[start] = 0
    fScore[start] = h(start, goal)  // 启发式估计
    
    while openSet is not empty:
        current = node in openSet with smallest fScore
        if current == goal:
            return reconstructPath(cameFrom, current)
        
        openSet.remove(current)
        for each neighbor of current:
            tentative_g = gScore[current] + dist(current, neighbor)
            if tentative_g < gScore[neighbor]:
                gScore[neighbor] = tentative_g
                fScore[neighbor] = tentative_g + h(neighbor, goal)
                cameFrom[neighbor] = current
                if neighbor not in openSet:
                    openSet.add(neighbor)
    return failure

避坑指南:我曾经因为启发函数选错了,导致 A* 搜出来的路径明显不是最优。后来才发现,启发函数必须满足“可容许性”——也就是估计距离不能超过实际距离。否则 A* 就退化成 Dijkstra 了,甚至更差。

4.2 采样算法:RRT 与 RRT*

图搜索虽然好,但有个硬伤——它需要把环境离散化。如果环境是连续的高维空间(比如机械臂的关节空间),栅格化会带来“维度灾难”。这时候,采样算法就派上用场了。

4.2.1 RRT:快速探索随机树

RRT 的思路很暴力:随机采样,然后往采样点方向生长。说白了就是“瞎蒙,但蒙得有策略”。

我记得第一次在仿真里跑 RRT,看到那棵树像藤蔓一样在障碍物之间蜿蜒生长,还挺有成就感的。但后来发现,RRT 有个毛病——它找到的路径往往很“绕”,不是最优的。

RRT 的核心步骤:

  1. 在自由空间随机采样一个点 q_rand
  2. 在树上找到离 q_rand 最近的节点 q_near
  3. 从 q_near 向 q_rand 方向生长一步,得到 q_new
  4. 如果 q_new 到 q_near 的路径无障碍,就把 q_new 加入树中
  5. 重复直到树到达目标区域
// 伪代码:RRT 核心逻辑
function RRT(start, goal, environment):
    tree = {start}
    for i = 1 to maxIterations:
        q_rand = randomSample()
        q_near = nearestNeighbor(tree, q_rand)
        q_new = steer(q_near, q_rand, stepSize)
        if collisionFree(q_near, q_new, environment):
            tree.add(q_new)
            if distance(q_new, goal) < threshold:
                return extractPath(tree, q_new)
    return failure

4.2.2 RRT*:让路径更优

RRT* 是 RRT 的升级版。它多做了两件事:重新选择父节点(rewire)和重新布线。说白了,就是每加入一个新节点,都看看能不能“抄近道”。

我在做无人机避障时对比过 RRT 和 RRT*。同样的场景,RRT 跑了 500 次迭代找到一条路径,RRT* 跑了 800 次迭代,但路径长度短了 30%。嗯,这个 trade-off 值得考虑。

我的经验:如果对实时性要求极高(比如每秒要规划一次),RRT 更合适。如果对路径质量有要求(比如节省燃料),那就用 RRT*。当然,现在也有 RRT*-Connect、Informed RRT* 这些变种,大家感兴趣可以自己研究。

4.3 路径平滑与优化

算法规划出来的路径,往往是折线——一段一段的,拐角尖锐。这样的路径,飞控根本没法直接跟踪。为什么?因为无人机有惯性,急转弯会掉高度,甚至失控。

路径平滑,说白了就是把折线变成曲线。常用的方法有:

  • B 样条曲线:控制点少,局部可调,适合在线使用
  • 贝塞尔曲线:简单直观,但全局控制,改一个点整条线都变
  • 多项式插值:适合做轨迹规划,可以同时约束位置、速度、加速度

平滑的核心原则:路径要连续,一阶导数(速度)要连续,二阶导数(加速度)最好也连续。这样飞控跟踪起来才顺滑。

// 示例:用三次样条做路径平滑
// 输入:路径点列表 waypoints = [(x0,y0), (x1,y1), ..., (xn,yn)]
// 输出:平滑后的轨迹参数

function cubicSplineSmooth(waypoints):
    n = len(waypoints) - 1
    // 构建三对角矩阵求解样条系数
    // 这里省略具体数学推导,实际工程中常用现成库
    // 比如 scipy.interpolate.CubicSpline
    return splineCoefficients

避坑指南:我曾经在平滑时没考虑障碍物,结果平滑后的路径穿过了障碍物。后来我加了一步“碰撞检测 + 局部重规划”,才算解决问题。记住:平滑不能以牺牲安全性为代价。

4.4 小结

这一章咱们聊了路径规划的四大金刚:Dijkstra 适合小地图、A* 适合有启发信息的场景、RRT 适合高维空间、RRT* 适合追求路径质量的场景。最后还讲了路径平滑——让规划出来的路径真正能用。

说实话,这些算法每个都值得单独开一章讲。但作为入门,大家先掌握核心思想,后面实战中再慢慢深入。下一章咱们会把这些算法放到真机上去跑,到时候你们就知道“纸上得来终觉浅”是什么意思了。


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