A*算法
一、A*算法
(一)算法思路
A*算法通过下面这个函数来计算每个节点的优先级。
f(n)=g(n)+h(n)
其中:
- f(n) 是节点n的综合优先级。当我们选择下一个要遍历的节点时,我们总会选取综合优先级最高(值最小)的节点。
- g(n) 是节点n距离起点的代价。
- h(n) 是节点n距离终点的预计代价,这也就是A*算法的启发函数。
A* 算法在运算过程中,每次从优先队列中选取 f(n) 值最小(优先级最高)的节点作为下一个待遍历的节点。
另外,A*算法使用两个集合来表示待遍历的节点,已经遍历过的节点,这通常称之为open_set和close_set。
(二)图片展示
二、启发函数
(一)启发函数影响
启发函数会影响A*算法的行为。
- 在极端情况下,当启发函数h(n)始终为0,则将由g(n)决定节点的优先级,此时算法就退化成了Dijkstra算法。
- 如果h(n)始终小于等于节点n到终点的代价,则A* 算法保证一定能够找到最短路径。但是当h(n)的值越小,算法将遍历越多的节点,也就导致算法越慢。
- 如果h(n)完全等于节点n到终点的代价,则A* 算法将找到最佳路径,并且速度很快。可惜的是,并非所有场景下都能做到这一点。因为在没有达到终点之前,我们很难确切算出距离终点还有多远。
- 如果h(n)的值比节点n到终点的代价要大,则A*算法不能保证找到最短路径,不过此时会很快。 在另外一个极端情况下,如果h(n)相较于g(n)大很多,则此时只有h(n)产生效果,这也就变成了最佳优先搜索。
(二)启发函数使用
对于网格形式的图,有以下这些启发函数可以使用
- 如果图形中只允许朝上下左右四个方向移动,则可以使用曼哈顿距离(Manhattan distance)。
- 如果图形中允许朝八个方向移动,则可以使用对角距离。
- 如果图形中允许朝任何方向移动,则可以使用欧几里得距离(Euclidean distance)。
(三)关于距离
1.曼哈顿距离
如果图形中只允许朝上下左右四个方向移动,则启发函数可以使用曼哈顿距离,它的计算方法如下图所示:
计算曼哈顿距离的函数如下,这里的LINECOST是指两个相邻节点之间的直线移动代价,通常是一个固定的常数。
function heuristic(node) = dx = abs(node.x - goal.x) dy = abs(node.y - goal.y) return LINECOST * (dx + dy)
2.对角距离
如果图形中允许斜着朝邻近的节点移动,则启发函数可以使用对角距离。它的计算方法如下:
计算对角距离的函数如下,这里的SLASHCOST指的是两个斜着相邻节点之间的移动代价。
function heuristic(node) = dx = abs(node.x - goal.x) dy = abs(node.y - goal.y) return LINECOST * (dx + dy) + (SLASHCOST - 2 * LINECOST) * min(dx, dy)
3.欧几里得距离
如果图形中允许朝任意方向移动,则可以使用欧几里得距离。
欧几里得距离是指两个节点之间的直线距离,因此其计算方法也是我们比较熟悉的:
function heuristic(node) = dx = abs(node.x - goal.x) dy = abs(node.y - goal.y) return COST * sqrt(dx * dx + dy * dy)
三、A*算法最优性问题的提出及证明
If h(n) is always lower than (or equal to) the cost of moving from n to the goal, then A* is guaranteed to find a shortest path.
The lower h(n) is, the more node A* expands, making it slower.
以上证明步骤,应该将5、6调换顺序看比较通顺。因为假设了g(s)是次优的,所以一定有一个更优的状态s'在open表中,于是有6的推断。但是由于选择的是s而不是s',所以又有5的推断,两者相互矛盾,所以g(s)一定是最优的。
四、实现
#include <iostream> #include <vector> using namespace std; //行数 Y轴 #define ROWS 10 //列数 X轴 #define COLS 10 //直线代价 #define LINECOST 10 //斜线代价 #define SLASHCOST 14 //点结构 点类 struct MyPoint { int y, x; int f, g, h; }; //枚举类型 就是为了代码可读性 enum dir { p_up, p_down, p_left, p_right, p_lup, p_ldown, p_rup, p_rdown }; //树节点类型 struct TreeNode { MyPoint pos; TreeNode* parent; vector<TreeNode*> child; TreeNode(MyPoint p) { pos.y = p.y; pos.x = p.x; pos.g = p.g; parent = NULL; } }; //计算H值并返回 int getManhattan(MyPoint pos, MyPoint end) { int x = ((pos.x > end.x) ? (pos.x - end.x) : (end.x - pos.x)); int y = ((pos.y > end.y) ? (pos.y - end.y) : (end.y - pos.y)); return (LINECOST * (x + y)); } int main() { //二维数组描述地图 int totalMap[ROWS][COLS] = {//0表示路 1表示障碍 { 0, 0, 0, 0, 1, 0, 0, 0, 0, 0 }, { 0, 0, 1, 1, 1, 0, 0, 0, 0, 0 }, { 0, 0, 1, 1, 1, 0, 0, 0, 0, 0 }, { 0, 0, 1, 0, 1, 0, 0, 0, 0, 0 }, { 0, 0, 1, 0, 1, 0, 0, 0, 0, 0 }, { 0, 0, 1, 0, 1, 0, 0, 0, 0, 0 }, { 0, 0, 1, 0, 1, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 1, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 1, 0, 0, 0, 0, 0 } }; MyPoint begPos = { 0, 0 }; MyPoint endPos = { 7, 6 }; //二维数组记录是否走过 // 0 false 表示没有走过 1 true 走过 bool pathMap[ROWS][COLS] = { 0 };//所有的点都是0 都没有走过 //标记起点走过 pathMap[begPos.y][begPos.x] = true; //创建一颗空树 TreeNode* rootNode = NULL; rootNode = new TreeNode(begPos);//起点入树 //准备一个数组 用来找最小的f vector<TreeNode*> buff; //当前点 TreeNode* pCurrent = rootNode; TreeNode* pChild = NULL; bool isFindEnd = false; while (1) { //1 把八点都做出来 for (int i = 0; i < 8; i++) { pChild = new TreeNode(pCurrent->pos); switch (i) { case p_up: pChild->pos.y--; pChild->pos.g += LINECOST; break; case p_down: pChild->pos.y++; pChild->pos.g += LINECOST; break; case p_left: pChild->pos.x--; pChild->pos.g += LINECOST; break; case p_right: pChild->pos.x++; pChild->pos.g += LINECOST; break; case p_lup: pChild->pos.x--; pChild->pos.y--; pChild->pos.g += SLASHCOST; break; case p_rup: pChild->pos.x++; pChild->pos.y--; pChild->pos.g += SLASHCOST; break; case p_ldown: pChild->pos.x--; pChild->pos.y++; pChild->pos.g += SLASHCOST; break; case p_rdown: pChild->pos.x++; pChild->pos.y++; pChild->pos.g += SLASHCOST; break; } if (pChild->pos.y >= 0 && pChild->pos.y < ROWS && pChild->pos.x >= 0 && pChild->pos.x < COLS && //没有走过 pathMap[pChild->pos.y][pChild->pos.x] == false && //不是障碍 totalMap[pChild->pos.y][pChild->pos.x] == 0 ) {//能走 //2 算出来h 和f值 放到树里 放到buff里 pChild->pos.h = getManhattan(pChild->pos, endPos);//算出来h //算出来f pChild->pos.f = pChild->pos.g + pChild->pos.h; //放到buff里 buff.push_back(pChild); //入树 pCurrent->child.push_back(pChild); pChild->parent = pCurrent; } else {//不能走 delete pChild; } } vector<TreeNode*>::iterator it; vector<TreeNode*>::iterator itMin; //3 找出buff中 f最小的点 走 删掉 itMin = buff.begin();//假设第一个最小 for (it = buff.begin(); it != buff.end(); it++) {//遍历整个buff数组 itMin = (((*itMin)->pos.f < (*it)->pos.f) ? itMin : it); } //走 pCurrent = *itMin; //删掉 buff.erase(itMin); //4 如果找到了终点 循环结束 if (pCurrent->pos.y == endPos.y && pCurrent->pos.x == endPos.x) { isFindEnd = true; break; } //5 buff空了 循环结束 if (buff.empty() == true) { break; } } //输出路径 看一看 if (isFindEnd) { cout << "找到终点了:"; while (pCurrent) { cout << "(" << pCurrent->pos.y << "," << pCurrent->pos.x << ")"; pCurrent = pCurrent->parent; } cout << endl; } else { cout << "没有找到路径" << endl; } return 0; }
五、Dijkstra算法和A*算法
Dijkstra算法和A*都是最短路径问题的常用算法,下面就对这两种算法的特点进行一下比较。
- Dijkstra算法计算源点到其他所有点的最短路径长度,A*关注点到点的最短路径(包括具体路径)。
- Dijkstra算法建立在较为抽象的图论层面,A*算法可以更轻松地用在诸如游戏地图寻路中。
- Dijkstra算法的实质是广度优先搜索,是一种发散式的搜索,所以空间复杂度和时间复杂度都比较高。对路径上的当前点,A*算法不但记录其到源点的代价,还计算当前点到目标点的期望代价,是一种启发式算法,也可以认为是一种深度优先的算法。
- 由第一点,当目标点很多时,A*算法会带入大量重复数据和复杂的估价函数,所以如果不要求获得具体路径而只比较路径长度时,Dijkstra算法会成为更好的选择。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了