第四讲:路径规划算法入门——A*算法原理、Dijkstra算法、栅格地图构建
各位同学,今天咱们进入路径规划的核心地带。说实话,我刚入行那会儿,觉得路径规划就是“找条路走过去”,后来在植保机器人项目里被现实狠狠教育了一顿——田里有电线杆、有水沟、有没长齐的庄稼,随便一条路都可能让机器卡住。嗯,今天讲的这两个算法,就是解决“怎么走最划算”这个问题的。
4.1 栅格地图:把现实世界变成格子
在做路径规划之前,得先把环境“翻译”成机器能懂的语言。我个人习惯用栅格地图,说白了就是把农田拍扁,切成一个个小方格。
栅格地图的构建思路:
- 每个格子代表一块区域,比如1米×1米
- 格子分两种:可通行(庄稼间隙、空地)和不可通行(障碍物、边界)
- 用0表示空地,1表示障碍物,存成一个二维数组
举个例子,一块10米×10米的田,有3个障碍物,地图大概长这样:
// 栅格地图示例(10x10)
int map[10][10] = {
{0,0,0,0,0,0,0,0,0,0},
{0,0,0,1,0,0,0,0,0,0}, // 障碍物1
{0,0,0,1,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,1,1,0,0,0}, // 障碍物2和3
{0,0,0,0,0,1,1,0,0,0},
{0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0}
};
我的经验:栅格大小很关键。太大容易漏掉小障碍物,太小计算量爆炸。植保作业我一般用0.5米格子,既能避开田埂,又不会让算法跑太久。
4.2 Dijkstra算法:老实人的最短路径
Dijkstra算法,你想想看,它就像个老实巴交的快递员——从起点开始,一层一层往外扩,直到找到终点。它保证找到的路径是全局最短的,但代价是慢。
核心思想:
- 从起点出发,标记起点距离为0,其他点距离为无穷大
- 每次选一个距离最小的未访问点,更新它邻居的距离
- 重复直到所有点都访问过
代码实现(C语言风格):
#define INF 99999
#define N 100 // 栅格数量
void dijkstra(int map[N][N], int start, int end) {
int dist[N]; // 最短距离
int visited[N]; // 是否已访问
int prev[N]; // 前驱节点
// 初始化
for (int i = 0; i < N; i++) {
dist[i] = INF;
visited[i] = 0;
prev[i] = -1;
}
dist[start] = 0;
for (int count = 0; count < N-1; count++) {
// 找当前距离最小的未访问节点
int u = -1, minDist = INF;
for (int i = 0; i < N; i++) {
if (!visited[i] && dist[i] < minDist) {
minDist = dist[i];
u = i;
}
}
if (u == -1) break; // 所有可达点已访问
visited[u] = 1;
// 更新邻居
for (int v = 0; v < N; v++) {
if (!visited[v] && map[u][v] > 0 &&
dist[u] + map[u][v] < dist[v]) {
dist[v] = dist[u] + map[u][v];
prev[v] = u;
}
}
}
}
我曾经踩过的坑:在嵌入式平台上跑Dijkstra,如果地图有1000个格子,每次找最小距离的循环就是O(n²),MCU直接卡死。后来我改用优先队列(堆)优化,复杂度降到O(n log n),这才跑起来。
4.3 A*算法:带“方向感”的寻路
A*算法比Dijkstra聪明在哪?它有个启发式函数,说白了就是“猜一下终点在哪边”。Dijkstra是四面开花,A*是朝着终点方向优先探索。
A*的核心公式:
f(n) = g(n) + h(n)
- g(n):从起点到当前点的实际代价
- h(n):从当前点到终点的估计代价(启发式)
- f(n):总估计代价,越小越优先探索
启发式函数怎么选?我常用曼哈顿距离(只能上下左右走时)或欧几里得距离(可以斜着走时)。
// A*算法核心逻辑(伪代码)
typedef struct {
int x, y;
int g, h, f;
Node* parent;
} Node;
Node* a_star(Node* start, Node* end, int map[][]) {
openList.push(start); // 待探索列表
closedList.clear(); // 已探索列表
while (!openList.empty()) {
Node* current = openList.popMinF(); // 取f值最小的
if (current == end) {
return reconstructPath(current); // 回溯路径
}
closedList.push(current);
// 遍历四个邻居
for (每个邻居 neighbor) {
if (neighbor 是障碍物 || neighbor 在 closedList)
continue;
int tentative_g = current->g + distance(current, neighbor);
if (neighbor 不在 openList) {
neighbor->g = tentative_g;
neighbor->h = heuristic(neighbor, end);
neighbor->f = neighbor->g + neighbor->h;
neighbor->parent = current;
openList.push(neighbor);
} else if (tentative_g < neighbor->g) {
// 找到更短路径,更新
neighbor->g = tentative_g;
neighbor->f = neighbor->g + neighbor->h;
neighbor->parent = current;
}
}
}
return NULL; // 无路可走
}
关键区别:Dijkstra只考虑g(n),A*同时考虑g(n)+h(n)。所以A*在大多数情况下比Dijkstra快得多,尤其在大地图上。
4.4 两种算法的对比
| 特性 | Dijkstra算法 | A*算法 |
|---|---|---|
| 保证最短路径 | 是 | 是(h(n)可采纳时) |
| 搜索方向 | 均匀向外扩散 | 朝向终点方向 |
| 计算速度 | 慢(O(n²)) | 快(取决于h(n)质量) |
| 适用场景 | 小地图、需要绝对最优 | 大地图、实时性要求高 |
| 嵌入式实现难度 | 简单 | 中等(需要优先队列) |
4.5 植保作业中的实际选择
我在做植保机器人时,遇到过这么个情况:田块有50米×30米,栅格化后1500个格子。用Dijkstra跑一次路径要200ms,而A*只要30ms。你想想看,机器人要实时避障,200ms的延迟可能就撞上突然出现的树枝了。
我的建议:
- 如果地图小(< 500格子),用Dijkstra,代码简单不容易出bug
- 如果地图大(> 500格子),必须上A*,否则MCU扛不住
- 启发式函数选曼哈顿距离,计算量小,适合嵌入式
避坑指南:我曾经在A*里用了欧几里得距离,结果因为浮点运算太慢,反而比Dijkstra还慢。后来改成曼哈顿距离(只用整数加减),速度直接翻倍。嵌入式上能不用浮点就别用。
好了,这一讲的内容就是这些。下节课咱们会把这些算法真正跑在STM32上,看看实际效果。记住,理论再漂亮,不上板子跑一跑都是纸上谈兵。