Astar搜索算法简介及保姆级代码解读
A*搜索算法简介及保姆级代码解读
1. A*算法简单介绍
A*算法是一种路径规划算法,和传统的Dijkstra算法有所不同,该算法有选择地进行节点搜索,因此比Dijkstra算法更快、搜索的点更少。
阅读本文,不需要掌握Dijkstra算法的知识,请放心食用。
注意:本文只介绍二维A*算法及相关示例。
PS:本文代码编写参考B站up主
小黎的Ally
的路径规划与轨迹跟踪系列算法学习_第4讲_A*算法,讲解详细,本文代码部分是将其代码进行了些许改动并加以解释,在此对up主的辛苦表达感谢!!
PPS:本文所使用的把地图栅格化的函数来自于博客
Matlab中将一幅图片做成栅格地图,本文进行了些许改动,再次一并感谢博主的辛苦!
1.1 A*算法理论基础
A*算法首先将要搜索的区域划分为若干栅格(grid),并有选择地标识出障碍物(Obstacle)与空白区域。一般地,栅格划分越细密,搜索点数越多,搜索过程越慢,计算量也越大;栅格划分越稀疏,搜索点数越少,相应地搜索精确性就越低。
如上图,引入地图信息后画出栅格,该图片采用 100 × 100 100 \times 100 100×100的栅格划分,图中黑色区域为障碍物区域。图中绿点为起始点,红点为终点。
对每个节点,在计算时同时考虑两项代价指标:当前节点与起始点的距离,以及当前节点与目标点的距离:
f = g + h f = g + h f=g+h其中 f f f为总代价, g g g为当前节点距离起始点的距离, h h h为当前节点距离目标点的距离。
而在计算距离时,又可以采用两种方式:
欧氏距离:
L = ( x 1 − x 2 ) 2 + ( y 1 − y 2 ) 2 L = \sqrt{\left( x_1 - x_2 \right) ^2 + \left( y_1 - y_2 \right) ^2} L=(x1−x2)2+(y1−y2)2 曼哈顿距离:
L = ∣ x 1 − x 2 ∣ + ∣ y 1 − y 2 ∣ L = \lvert x_1 - x_2 \rvert + \lvert y_1 - y_2 \rvert L=∣x1−x2∣+∣y1−y2∣为了计算方便,本文计算 h h h时采用曼哈顿距离,这也是A*算法中的一贯做法。
为了方便计数,在计算每一个节点时,在栅格左上角写 f f f值,左下角写 g g g值,右下角写 h h h值。
1.1.1 节点计算
这里举一个例子。
如图所示,A点为起始点,M点为终点。对于A点来说,对其周边的8个节点进行寻找,A点本身为父节点,周边8个点为子节点。
假设:
- 格子边长为10,这样水平和竖直位移一格为10,而对角位移一格为14;
- 从父节点到子节点可以水平、竖直、对角线位移计算 g g g值,而从子节点到目标点只能使用水平、竖直位移计算曼哈顿距离 h h h值。
对于点B:从A到B只需右移一格,因此B的 g = 10 g=10 g=10;从B到M需要先右移四格,再上移三格,因此B的 h = 40 + 30 = 70 h = 40+30=70 h=40+30=70。这样B的 f = g + h = 10 + 70 = 80 f=g+h=10+70=80 f=g+h=10+70=80。将三者都记在B格中。
对于点C:从A到C只需向右上方平移一格,因此C的 g = 14 g=14 g=14;从C到M需要先右移四格,再上移两格,因此C的 h = 40 + 20 = 60 h=40+20=60 h=40+20=60,继而C的 f = g + h = 74 f=g+h=74 f=g+h=74。
同理可以计算出D的 g = 14 , h = 80 , f = 94 g=14, h=80,f=94 g=14,h=80,f=94,以及其他几个子节点的值。
将这8个子节点进行对比,可以发现,C点的 f f f值最小,因此选取C点为下一步搜寻的父节点, A C ‾ \overline{AC} AC即为路径迈出的第一步。
如图所示,现在将C作为新的父节点。
对于J点:
从C到J只需右移一格,因此J的 g = 10 + g C = 10 + 14 = 24 g=10+g_C =10+14=24 g=10+gC=10+14=24。需要注意的是,J点的 g g g值是从C到J的 g g g加上从A到C的 g g g,即当前子节点的 g g g值是从起点到该点的所有 g g g累和。
从J到M需要先右移三格,再上移两格,因此J的 h = 30 + 20 = 50 h = 30+20=50 h=30+20=50。这样J的 f = g + h = 24 + 50 = 74 f=g+h=24+50=74 f=g+h=24+50=74。将三者都记在J格中。
对于I点:
从C到I只需向右上方平移一格,同样地,I点的 g g g值是从C到I的 g g g加上从A到C的 g g g,即当前子节点的 g g g值是从起点到该点的所有 g g g累和,因此I的 g = 14 + g C = 14 + 14 = 28 g=14+g_C =14+14=28 g=14+gC=14+14=28。
从I到M需要先右移三格,再上移一格,因此I的 h = 30 + 10 = 40 h = 30+10=40 h=30+10=40。这样I的 f = g + h = 28 + 40 = 68 f=g+h=28+40=68 f=g+h=28+40=68。将三者都记在I格中。
同样计算出其他点的值。
可以看出,I点的 f f f值最小,因此选择I点作为路径上的下一个点,此时路径变为 A C I ‾ \overline{ACI} ACI。
如此一步步进行迭代,最后找到最优轨迹。
1.1.2 由计算得出的小结论
- 每个节点中需要存储至少3个值: g , h , f g,h,f g,h,f。
- 当子节点迭代到目标点本身时, h = 0 h=0 h=0,即当前节点到目标点的距离为0.
- 在算法实施时, g g g的计算可以取10或14,即从父节点到子节点可以水平/竖直位移,也可以沿对角线位移;而 h h h的计算只能取10的倍数,即从当前子节点到目标点只能水平/竖直位移。前者可以采用欧氏距离,后者只能采用曼哈顿距离。
- 当某个子节点被选中后,就作为下一次搜索的父节点;并且为了避免节点的重复计算和筛选,在下一次搜索时,需要将上一步选中的节点从“可搜索”列表中删除。如上图中,当C为父节点时,选中I作为下一次的父节点;那么当第二步I为父节点时,由于C已经在已选轨迹上了,所以I的子节点实际上并不包括C,只计算7个子节点即可。
- 鉴于上一点,可以预想,算法的具体实施过程中,需要有两个数组保存“待计算子节点”和“已被选中的节点”。当“待计算子节点”中某个点符合 f f f最小时,就将其加入“已选中的节点”,并在“待计算子节点”中删除该点,防止后续重复计算。
- 当算法结束时,保存在“已选中的节点”中的所有点,按照顺序即为找出的路径。
- 算法结束时,最后一个点的 f f f值,即为从起点到终点所用的距离。
- 算法的停止条件:1)当“待计算子节点”中没有点时,即已经没有点可供寻找了;2)当当前父节点恰为目标节点时,即 h = 0 h=0 h=0。
1.2 算法逻辑结构
A*算法的逻辑结构如下:
1)初始化。导入地图信息,设置障碍物区域,设置起点start
、终点target
、栅格数量 m × n m \times n m×n等。
2)数据预处理。建立“待计算子节点”的数组openlist
,“已选中的节点”的数组closelist
,保存路径的数组closelist_path
。除此之外,还需建立一个当前子节点集合children
,用来保存当前父节点周围8个子节点的坐标,以及父节点本身parent
;还有保存代价值 g , h , f g, h,f g,h,f的数组openlist_cost
和closelist_cost
。
3)对子节点们children
中的每个节点child
:
若该子节点不在“待计算子节点”节点openlist
中,则追加进去;
若在,则计算出该child
的 g g g值,该 g g g值是从起点到父节点parent
的距离加上父节点到该子节点的距离。若该 g g g值小于之前openlist_cost
中的 g g g最小值,那么就将openlist_cost
中的最小 g g g值更新;
4)由于该代价最小点已经加入了轨迹,因此将该点加入clost_list
和closelist_path
,并从openlist
中剔除;
5)更新openlist
中的最小代价值,并以其为父节点开始新一轮搜索。
2. 代码解析
2.1 引入地图
代码块:
1 | %% 画地图 |
绘制一个 150 × 150 150 \times 150 150×150的地图,起点设置为 ( 10 , 20 ) (10,20) (10,20),终点设置为 ( 130 , 80 ) (130,80) (130,80)。
障碍物的坐标通过函数TrunToGridMap(m, n)
获得:
1 | function obs = TrunToGridMap(a, b) |
该函数读取一个图片文件,将其转化为灰度图像,并将灰度图中黑色色块所在的坐标返回为障碍物坐标。
2.2 预处理
1 | %% 预处理 |
这一部分主要做了以下几步:
- 把起点
start
设为轨迹的第一个点:closelist = start
; - 路径初始化,即从起点到起点:
closelist_path = {start, start}
; - 代价首先置为0:
closelist_cost = 0
; - 利用
child_nodes_cal
函数计算当前父节点(即起点)周围的子节点们; - 这些子节点们
children
即为待计算的子节点,也就是openlist
; - 随后进入一个循环,对每一个子节点i,
openlist_path
中第i行第1个元素储存第i个节点child
,第2个元素为一个列向量,分别是第i个child
的起点和它本身; - 第二个循环则是计算代价的循环,对每一个子节点i,计算 g g g(这里使用范数
norm
), h h h(曼哈顿距离)和 f f f;之后,在代价数组openlist_cost
中储存这三个代价值,因此openlist_cost
第i行的元素为一个行向量;
其中用到的子节点计算函数如下:
1 | function child_nodes = child_nodes_cal(parent_node, m, n, obs, closelist) |
2.3 定义父节点parent
1 | %% 定义父节点 |
这一步搜索openlist_cost
代价数组中 f f f最小的元素,记下其索引min_idx
;
随后该索引对应的openlist
中的元素即为“ f f f值最小的待计算子节点”,作为下一步计算的父节点parent
。
2.4 主循环
1 | %% 进入循环 |
2.4.1
对这一部分循环来说,首先利用child_nodes_cal
函数得出当前父节点parent
周围的子节点们children
;
对每一个子节点child
,判断其是否在“待计算子节点”openlist
列表中——in_flag
=1表示在列表中,同时openlist_idx
为该子节点在openlist
中的索引号。
判断完成后,首先计算该子节点的 g , h , f g,h,f g,h,f值,注意: g g g值为父节点parent
的 g g g加上该child
子节点的 g g g值。
2.4.2
计算 g , h , f g,h,f g,h,f之后,再来看子节点在openlist
中的情况。由于循环中查看了parent
周围所有子节点的情况,所以一定会存在某个子节点的 g g g比其他子节点的 g g g都小。如果找到了这样的子节点,那么就更新该子节点在openlist_cost
中对应位置的 g , h , f g,h,f g,h,f值。
同时,还要将该子节点加入到路径openlist_path
中,即这一句代码
1 | openlist_path{openlist_idx, 2} = [openlist_path{min_idx, 2}; child]; |
这行代码的含义是:
openlist_idx
表示当前子节点child
在openlist
中的索引,min_idx
表示之前所有子节点中代价最小的子节点的索引。之前在预处理一节中提到,openlist_path
的第i行第2列为一个列向量,分别是父节点和当前child,相当于第2列储存了路径。
而min_idx
表示“代价最小的节点”,也就是父节点parent
。因此这里openlist_path{min_idx, 2}
表示父节点的路径。而[openlist_path{min_idx, 2}; child]
则是把新的child
附加到这一个列向量上,相当于把新的child
附加到路径尾端,把向量长度延长了一个child
,延长了路径。
这样,[openlist_path{min_idx, 2}; child]
构成了一个“父节点路径-当前child”的新路径。
把这个新路径赋值给openlist_idx
索引所对应的openlist_path
上,即为该openlist_idx
索引对应的子节点的路径。
2.4.3
另一方面,如果该child
不在openlist
中,那么就把该child
添加到“待计算子节点”列表中,其 g , h , f g,h,f g,h,f值添加到openlist_cost
代价列表中,其路径[openlist_path{min_idx, 2}; child]
添加到路径openlist_path
中。
2.4.4
由于移动代价最小的节点已经是路径上的一点了,所以为了避免重复计算,应当把她从“待计算子节点”列表中删除,加入“已计算节点”中,即
1 | closelist(end+1, :) = openlist(min_idx, :); |
仔细观察不难发现,min_idx
对应的正是这一步的parent
节点,直到这里,我们才把它加入到“已计算节点”列表中,在之前它一直呆在openlist
中。
2.4.5
父节点加入到了“已计算节点”中了,那么下一步就没有父节点了,所以需要找出新的父节点:
1 | [~, min_idx] = min(openlist_cost(:, 3)); |
同时还需要判断一下,是否已经进行到了程序结束:
1 | % 判断是否搜索到终点 |
需要注意,即使当当前父节点已经是目标点了,也不要忘了把这个父节点加入到“已计算节点”中,相当于把目标点添加入路径中,形成路径上最后一个点。
2.5 画图
这一步就是将结果绘制出来了:
1 | path_opt = closelist_path{end, 2}; |
3. 结果
这里导入稻妻地图作为路径规划的示例,该地图多为岛屿,路径复杂,障碍物分散且数量多,十分适合作为示例使用。
将其灰度化得到
把地图划分为 m × n = 150 × 150 m \times n = 150 \times 150 m×n=150×150的栅格,并设置起点为 ( 10 , 20 ) (10,20) (10,20),终点为 ( 130 , 80 ) (130,80) (130,80):
图中绿色为起点,红色为终点。
路径规划结果:
可以看出,A* 算法可以找到期望路径。
文件所有代码在笔者的资源二维A*算法路径规划matlab代码中可以下载得到。