2. 地图建模:栅格地图的构建与数据结构设计

做路径规划,第一步不是写算法,而是先把“世界”描述清楚。

我刚开始接触A*算法时,踩过最大的坑就是——地图没建好,算法跑得再漂亮也是白搭。你想想看,机器人连自己周围是墙还是路都搞不清,它怎么规划路径?

所以这一章,咱们就聊聊地图建模。说白了,就是把真实环境翻译成计算机能理解的数据结构。

2.1 栅格地图的基本概念

栅格地图(Grid Map)是目前最常用的地图表示方式之一。它的思路很简单:把环境切分成一个个小格子,每个格子要么是“可通过”,要么是“不可通过”。

我个人习惯用0表示空闲,1表示障碍物。为什么?因为这样在代码里判断起来最直观。

核心思想: 用二维数组存储地图,每个元素代表一个栅格的状态。

举个例子,一个5×5的简单地图:

// 0 = 空闲,1 = 障碍物
int map[5][5] = {
    {0, 0, 0, 0, 0},
    {0, 1, 1, 0, 0},
    {0, 0, 0, 0, 0},
    {0, 0, 1, 1, 0},
    {0, 0, 0, 0, 0}
};

嗯,这里要注意:地图的尺寸决定了算法的搜索空间。地图越大,搜索时间越长。我在项目中遇到过,一个1000×1000的地图,如果不做优化,A*跑起来能让你等到怀疑人生。

2.2 障碍物的表示方式

障碍物怎么表示?其实就两种思路:

  • 静态障碍物: 在初始化时直接写入地图数组
  • 动态障碍物: 运行时实时更新地图数据

我曾经在一个仓储机器人项目里,需要处理动态障碍物——比如其他机器人临时停靠。那时候我用的方法是:维护一个独立的障碍物列表,每次路径搜索前先更新地图。

避坑指南: 我曾经把动态障碍物直接写进地图数组,结果导致路径规划时地图被永久修改,后续搜索全乱套了。后来我学乖了——每次搜索前复制一份地图副本,在副本上操作。

障碍物还可以有“代价”属性。比如:

障碍物类型 代价系数 说明
空地 1.0 正常通行
草地 2.0 通行较慢
泥地 3.0 通行困难
墙壁 不可通行

你想想看,如果机器人走草地和走空地的代价一样,那它可能就会选择穿过草地——但实际场景中,草地可能更耗电、更容易打滑。所以给不同地形赋予不同代价,更符合真实情况。

2.3 四邻域与八邻域移动代价

这是很多初学者容易搞混的地方。我简单说一下:

  • 四邻域: 只能上下左右移动
  • 八邻域: 可以上下左右 + 四个对角线方向

移动代价怎么算?

四邻域很简单,每步代价固定为1。但八邻域就有讲究了——对角线移动的距离是√2 ≈ 1.414,所以代价不能和直线移动一样。

我建议: 直线移动代价设为10,对角线移动代价设为14。这样用整数计算,避免浮点数精度问题。

// 移动代价定义
const int STRAIGHT_COST = 10;   // 直线移动
const int DIAGONAL_COST = 14;   // 对角线移动

// 判断移动方向
if (dx == 0 || dy == 0) {
    cost = STRAIGHT_COST;
} else {
    cost = DIAGONAL_COST;
}

为什么会这样?因为A*算法中,代价计算直接影响路径质量。如果对角线代价设得太低,算法会倾向于走对角线,路径看起来会“飘”;设得太高,又可能绕远路。

注意: 八邻域移动时,要检查对角线是否被障碍物挡住。比如你要往右上角走,但右边和上边都有障碍物,那这个对角线移动就不合法。我曾经没做这个检查,结果机器人穿墙了……

2.4 地图数据结构设计

数据结构设计得好,代码写起来就顺手。我个人常用的结构是这样的:

// 栅格节点结构体
typedef struct {
    int x, y;           // 坐标
    int g_cost;         // 起点到当前点的代价
    int h_cost;         // 当前点到终点的估计代价
    int f_cost;         // g_cost + h_cost
    int is_obstacle;    // 是否为障碍物
    int parent_x;       // 父节点坐标(用于回溯路径)
    int parent_y;
} GridNode;

// 地图类
class GridMap {
private:
    int width, height;
    GridNode** nodes;   // 二维数组
    
public:
    GridMap(int w, int h);
    ~GridMap();
    bool is_valid(int x, int y);      // 检查坐标是否合法
    bool is_walkable(int x, int y);   // 检查是否可通行
    int get_cost(int x1, int y1, int x2, int y2);  // 计算移动代价
    void set_obstacle(int x, int y, bool value);    // 设置障碍物
};

嗯,这里要注意几点:

  • 内存管理: 大地图建议用一维数组模拟二维,减少内存碎片
  • 边界检查: 每次访问地图前都要检查坐标是否越界
  • 父节点记录: 这是路径回溯的关键,千万别漏了

个人经验: 我习惯把地图数据和搜索逻辑分开。地图只负责“存储和查询”,搜索算法只负责“计算和决策”。这样后期换算法(比如换成JPS)时,地图代码完全不用动。

2.5 知识体系总览

为了让你更直观地理解本章内容,我画了一张结构图:

栅格地图建模知识体系 栅格地图建模 地图构建 障碍物表示 移动代价 二维数组 尺寸定义 静态/动态 代价属性 四邻域 八邻域 数据结构设计 → 内存管理 → 边界检查 → 父节点记录 地图与搜索逻辑分离,便于后期扩展

这张图把本章的核心内容串起来了。从地图构建到障碍物表示,再到移动代价计算,最后落到数据结构设计——每一步都环环相扣。

好了,地图建模就聊到这儿。记住一句话:地图建得好,算法跑得顺。别嫌这一步麻烦,后面你会感谢自己花时间把地图设计清楚的。


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