57、C++项目实战:机器人路径规划(A*算法)

路径规划,说白了就是让机器人自己找到一条从起点到终点的路。别撞墙,别绕远,最好还能走最短路径。我最早接触这个问题是在一个仓储机器人项目里,当时小车在货架间穿梭,如果路径选不好,要么堵车,要么撞货架。嗯,后来我选了A*算法,效果还不错。

A*算法是启发式搜索的经典代表。它不像广度优先那样傻傻地往四周扩散,而是带着“方向感”去搜索。这个方向感,就来自一个评估函数。

核心思想:f = g + h

A*算法的灵魂就这一个公式:

f(n) = g(n) + h(n)
  • g(n):从起点到当前节点n的实际代价。说白了就是已经走了多远。
  • h(n):从当前节点n到终点的估计代价。这是启发式函数,是A*的“直觉”。
  • f(n):总估计代价。A*每次选f值最小的节点去扩展。

我刚开始学的时候,总觉得h(n)随便写个距离就行。后来踩了坑才发现,h(n)的选择直接影响算法效率和正确性。如果h(n)永远不大于真实距离,A*保证能找到最短路径。这叫“可采纳性”。

关键点:h(n) ≤ 真实距离 → A*保证最优解。h(n)越接近真实距离,搜索越快。

启发式函数怎么选?

不同的地图,适合不同的h(n)。我整理了一个表格,方便你对照:

地图类型 推荐启发式函数 说明
网格地图(四方向) 曼哈顿距离 |dx| + |dy|,只能上下左右走
网格地图(八方向) 切比雪夫距离 max(|dx|, |dy|),可以斜着走
任意方向移动 欧几里得距离 sqrt(dx² + dy²),直线距离
路网/图结构 直线距离 两点间直线,作为下界估计

我在仓储项目里用的是网格地图,机器人只能上下左右走,所以选了曼哈顿距离。你想想看,如果选了欧几里得距离,估计值可能比实际小太多,搜索范围会大得离谱。

数据结构:Open List 和 Close List

A*算法需要维护两个核心列表:

  • Open List:待探索的节点。每次从中取出f值最小的节点。
  • Close List:已经探索过的节点。避免重复走回头路。

Open List 的选型很关键。我见过有人用vector,每次手动找最小值。地图小还行,地图一大就卡死了。我个人习惯用优先队列(priority_queue),C++标准库就有,省心。

小技巧:C++的priority_queue默认是大顶堆,我们需要小顶堆。记得自定义比较器,或者存负值。

C++实现:核心代码

先定义节点结构:

struct Node {
    int x, y;          // 坐标
    int g;             // 起点到当前点的代价
    int h;             // 当前点到终点的估计代价
    int f;             // f = g + h
    Node* parent;      // 记录父节点,用于回溯路径

    Node(int x, int y) : x(x), y(y), g(0), h(0), f(0), parent(nullptr) {}
};

比较器,用于优先队列:

struct CompareNode {
    bool operator()(const Node* a, const Node* b) {
        return a->f > b->f;  // 小顶堆:f值小的优先
    }
};

A*主函数框架:

std::vector<Node*> aStarSearch(int startX, int startY, int endX, int endY,
                                const std::vector<std::vector<int>>& grid) {
    // 1. 初始化起点和终点
    Node* startNode = new Node(startX, startY);
    Node* endNode = new Node(endX, endY);

    // 2. 优先队列(Open List)
    std::priority_queue<Node*, std::vector<Node*>, CompareNode> openList;
    openList.push(startNode);

    // 3. Close List(用二维数组标记)
    std::vector<std::vector<bool>> closedList(grid.size(),
        std::vector<bool>(grid[0].size(), false));

    // 4. 方向数组(四方向)
    int dx[] = {-1, 1, 0, 0};
    int dy[] = {0, 0, -1, 1};

    while (!openList.empty()) {
        Node* current = openList.top();
        openList.pop();

        // 到达终点
        if (current->x == endX && current->y == endY) {
            return reconstructPath(current);
        }

        closedList[current->x][current->y] = true;

        // 探索四个邻居
        for (int i = 0; i < 4; i++) {
            int nx = current->x + dx[i];
            int ny = current->y + dy[i];

            // 边界检查、障碍物检查、是否已关闭
            if (nx < 0 || nx >= grid.size() || ny < 0 || ny >= grid[0].size())
                continue;
            if (grid[nx][ny] == 1)  // 1表示障碍物
                continue;
            if (closedList[nx][ny])
                continue;

            Node* neighbor = new Node(nx, ny);
            neighbor->g = current->g + 1;  // 每步代价为1
            neighbor->h = manhattanDistance(nx, ny, endX, endY);
            neighbor->f = neighbor->g + neighbor->h;
            neighbor->parent = current;

            openList.push(neighbor);
        }
    }

    // 没找到路径
    return {};
}

曼哈顿距离计算:

int manhattanDistance(int x1, int y1, int x2, int y2) {
    return abs(x1 - x2) + abs(y1 - y2);
}

路径回溯:

std::vector<Node*> reconstructPath(Node* node) {
    std::vector<Node*> path;
    while (node != nullptr) {
        path.push_back(node);
        node = node->parent;
    }
    std::reverse(path.begin(), path.end());
    return path;
}

A*算法流程图

下面这张图展示了A*算法的完整流程,从起点开始,一步步找到终点:

开始 初始化起点、终点、Open/Close列表 Open List 为空? (无路可走) 返回空路径 取出f值最小的节点 是终点? 回溯路径 扩展邻居节点 更新Open/Close列表

避坑指南

我曾经在一个项目中,Open List 里重复插入了同一个节点,导致死循环。后来我加了一个 visited 标记,每次从 Open List 取出节点时检查是否已经处理过。

还有几个常见问题:

  • 内存泄漏:new 出来的 Node 记得 delete。我建议用智能指针,或者用 vector 统一管理生命周期。
  • 障碍物边界:检查邻居时,一定要先判断坐标是否越界,再访问 grid 数组。顺序反了会崩。
  • 启发式函数不一致:如果 h(n) 有时大于真实距离,A* 可能找不到最短路径。我一般用曼哈顿距离,安全。

性能优化建议

地图大了之后,A* 的搜索空间会爆炸。我分享几个优化思路:

  1. 使用二叉堆:C++ priority_queue 底层就是堆,够用。如果追求极致性能,可以用斐波那契堆。
  2. 跳点搜索(JPS):在网格地图上,JPS 可以跳过大量对称路径,速度提升几十倍。我后来把项目改成了 JPS,效果立竿见影。
  3. 分层规划:先粗粒度规划大方向,再细粒度规划局部路径。适合超大场景。
  4. 缓存路径:如果起点和终点经常不变,可以缓存路径结果,避免重复计算。

我个人习惯:在开发阶段先用 A* 验证逻辑,确保正确性。性能不够时再换成 JPS 或分层规划。过早优化是万恶之源。

总结

A* 算法是路径规划的基石。它用 f = g + h 这个简单的公式,平衡了搜索的广度和方向性。你想想看,没有启发式函数,就是 Dijkstra;没有 g 值,就是贪心最佳优先。A* 把两者结合,既保证最优,又提高效率。

我在实际项目中,A* 的代码量其实不大,核心就几十行。但真正用好它,需要理解启发式函数的选择、数据结构的搭配、以及各种边界情况的处理。嗯,希望这篇文章能帮你少走弯路。


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