A*与JPS算法的背景和实现
Posted 森明帮大于黑虎帮
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了A*与JPS算法的背景和实现相关的知识,希望对你有一定的参考价值。
A*算法
背景
之前的DFS,BFS和Dijskra算法都是一种纯搜索的算法,实际使用时,算力占用很多,搜索的效率却不高。
先看一下BFS搜索(图片来源于网上),图片中红色点为起始点,蓝色点为终点,绿色点为搜索过的点。BFS以起点为圆心,先搜索周围的所有点,一圈一圈的搜索,直到搜索到蓝色的终点。
另外再看以下DFS搜索算法,DFS搜索的思路是朝着终点方向进行搜索,尽量靠近终点。
两者相比较的话,DFS因为向着终点移动,所以可以更快速的搜索到终点,但是这种快速的搜索默认的是起点和终点之间没有任何障碍物,且地形的权值都差不多。如果起点和终点之间有障碍物,那么DFS则会出现绕弯的情况。
从图中可以看出,DFS搜索时,如果遇到障碍物,没有办法保障找到的是一条最优的路径,只能保证搜索到一条可行的路径,或者搜索失败,没有路径。
所有,BFS的优点是,从起点到终点的移动代价最小,但是搜索的过程比较久,搜索的点更多;DFS的优点是,搜索的过程消耗的算力最小,搜索的时间更迅速。
结合了这两种算法的优点,A*算法就此诞生。
算法思想
A*的核心思想就是先进行一次BFS搜索,然后从这次BFS中找到距离目标点最近的点(这里就是DFS搜索的思想),然后在对这个点,分别进行一次BFS和DFS搜索,重复此操作直到到达目标点为止。
代价方程
设计代价方程计算公式F = G + H 中,G=起点到目标方格的移动代价,沿着起点到目标方格,这里设定水平或者垂直移动一次的代价是10,对角线移动一次为14。
H=从目标方格到终点的估算成本,计算目标方格到终点的代码,只计算水平和垂直移动的格数然后乘以10.
在一个方格中,将FGH分别标记在左上、左下和右下三个位置,以便每次观察估价结果。
搜索区域
如下图所示,假设需要从点A(绿色)移动到点B(红色),中间有一块蓝色区域为不可同行区域。
从图中可以看到,搜索算法的第一步就是先将要搜索的区域划分成正方形的格子,通过这种方式将搜索区域简化为了一个二维数据,数组每一项就是一个格子,每个格子就两种状态:可行区域,和不可行区域。
搜索步骤
-
新建两个list,一个open list,用于保存当前待处理的点,一个close list,用于保存当前不用考虑的点,先将起始点A放入open list,此时close list 为空。
-
将点A从open list取出,放入close list,此时将与点A相邻的八个点放入open list,分别计算各个点的F值,由图中可以看出F值最小的为40,将有最小F值的点放入close list,并将此点的父节点设置为点A。
-
将close list中最新放入的点设置为当前点,清空之前的open list,并将当前点附近的不在close list中且可以通行的点放入open list,并同时计算出当前open list中所有点的F值(这里设定遍历点的顺序为下、右下、右、右上、上、左上、左、左下),取出其中最小的F值的点放入close list,此时当前点的上下两个位置的点的F值大小相同,按照设定的顺序来,取下方的点。
-
此时如图所示,将第三个点的父节点设置为放入close list中的第二个点,此时看一下第三个点是不是与第一个点相邻,如果相邻的话,比较一下,当前节点父节点的父节点直接到当前节点的G值和当前节点父节点的父节点通过当前节点再到当前节点的G值的大小,这里有点绕,举个例子说一下,比如放入close list中的点目前有A,B,C,D四个点,当前点为D点,B->C->D的G值大小为20,同时点D与点B相邻,直接由B->D的G值为14,此时就将点D的父节点设置为点B,取消点C的父节点B,这样就有点A为点B的父节点,点B为点D的父节点。
-
不断重复第三步第四步,直到搜索到目标点或者到达边界,沿着目标点的父节点一直到起点,就是搜索出的移动路径。
A*算法总结
上面的步骤已经描述的算是很详细了,下面再过一遍整体的流程:
- 将起点加入close list。
- 将close list中最新添加的节点设置为当前节点,清空open list后,将当前节点的周围的点加入open list(其中的非搜索过的点,以及不可以到达的点除外),找出当前open list中F值最小的点加入close list。
- 查看当前节点的父节点是否有父节点,有父节点并且与当前节点相邻的话,计算以下直接由当前节点父节点的父节点直接到当前节点的G值,与当前节点父节点的父节点通过当前节点的父节点到达当前节点的G值进行比较,如果当前节点父节点的父节点直接到当前节点的G值较小,则将当前节点的父节点的父节点取消,将当前节点的父节点设置为原先父节点的父节点。
- 重复以上步骤,直到边界或者搜索到目标点。
不足之处:
- A*算法每次都需要维护openlist,如果地图很小的话,速度会很快,但是如果地图很大的话,搜索速度就会很慢,为了解决这个问题,可以使用二叉堆进行搜索。
- 路径不够平滑;
- 实际设置地图中,不要将格子设置的过小,这样的话会大大的增加算力的消耗。
JPS搜索
基本思想
JPS在A Star算法模型的基础之上,优化了搜索后继节点的操作。A星的处理是把周边能搜索到的格子,加进OpenList,然后在OpenList中弹出最小值,然后重复搜索直到找到终点或者搜索失败。JPS也是这样的操作,但相对于A星来说,JPS操作OpenList的次数很少,它会先用一种更高效的方法来搜索需要加进OpenList的点,然后在OpenList中弹出最小值,具体更高效的方法是什么。
首先要明确的一点就是A* 是JPS的基础,JPS 的思想是建立在A* 的基础上的,与A* 一样的,在寻路的过程中,将需要保存的点分别open list和close list,代价方程计算方法也如A* 一致。在此基础上新增加了强迫邻居与跳跃点的概念。
强迫邻居
首先看一下强迫邻居的定义,当一个节点x的八个邻居中有障碍,且x的父节点p经过x到达节点n的任意路径的距离代价最小,则称节点n是节点x的强迫邻居。字面上来理解,不是很直观,下面结合图片,对这个概念进行一些详细的说明。
在JPS的搜索中,强迫邻居的判断可以分为两种,一是水平搜索方向上,一是对角搜索方向上。
首先,介绍以下水平方向上的,如下图所示,可以看出下图中,点(7,10)为起点,进行水平搜索,到达点(9,10)时,发现红色点(9,11)为障碍点,(10,11)是可行点,因此,可以看出,从起点(7,10)经过点(9,10)后到达点(10,11)的距离最短,因此,满足了强迫邻居的几个要求:
- 有障碍点;
- 经过一个点到达障碍点旁边的节点的距离最小,且所到达的点是可行驶点;
因此,点(9,10)就是一个跳跃点,而点(10,11)则是点(9,10)的强迫邻居。
同理,点(10,9)也是点(9,10)的强迫邻居。
再来看一下,对角方向上的搜索,如下图所示,点(7,10)是搜索起点,向右下角进行搜索时,点(8,9)左边的点(7,9)是障碍点,且点(7,8)是可行走点,因此,就认为点(7,8)是跳跃点(8,9)的强迫邻居。
同理,下图中,点(8,10)是障碍点,(8,9)是可行走点的情况下,如果(9,10)是可行走点,则认为(9,10)是强迫邻居。
跳跃点
在上部分,阐述强迫邻居的概念时,其实也已经对跳跃点有了一个大概的解释,现在对跳跃点的概念进行一下总结:
- 起点和目标点是跳跃点;
- 如果点有强迫邻居则该点是跳跃点;
- 如果一点的父节点到该点是对角线移动,并且该点通过水平或者垂直移动可以到达跳跃点,则这个点就是跳跃点。
说通俗易懂一点,就是在该点发生移动方向的改变,该点就是跳跃点。
搜索规则
其实,从上述描述,个人总结JPS搜索的核心思想就是,先沿着代价方程最小的方向移动到障碍物附近,然后沿着障碍物找到障碍物的顶点,然后绕过去,因为直到目标点在哪,只要绕过了有障碍物的地方且向着目标点移动,总是能找到终点,就好像玩走迷宫的游戏一样。
下面是详述的搜索规则:
规则一, JPS 搜索跳点的过程中,如果直线方向(为了和对角线区分,直线方向代表水平方向、垂直方向)、对角线方向都可以移动,则首先在直线方向搜索跳点,再在对角线方向搜索跳点。
规则二, (1)如果从 parent到current 是直线移动,n 是 current 的邻居,若有从 parent到 n 的路径不经过 current且路径长度小于或等于从 parent经过 x 到 n 的路径,则走到 current 后下一个点不会走到 n;(2)如果从 parent到 current是对角线移动,n 是 current的邻居,若有从 parent到n 的路径不经过 current且路径长度小于从 parent经过 current 到 n 的路径,则走到 current 后下一个点不会走到 n。
规则三, 只有跳点才会加入 openset,因为跳点会改变行走方向,而非跳点不会改变行走方向,最后寻找出来的路径点也只会是跳点集合的子集。
举个栗子
还是上图先:
-
图 1:绿色为起点,红色为终点,黑色为障碍物。
-
图 2:由前面的讲解,分别向水平和竖直方向搜索,因为一直未发现我们感兴趣的节点,所以沿着对角线方向移动。
-
图3 :从黄色节点向水平方向搜索时,发现一个携带 forced neighbor 的节点(淡紫色),因此结束这次搜索,同时将图 3 中的- - 黄色节点加入 openlist ,绿色节点加入 closelist ,此时 openlist 中只有黄色节点。
-
图4 :从 openlist 中弹出黄色节点,沿着之前的方向(绿色 -> 黄色)开始新一轮的搜索,由于依旧是对角线方向,所以先竖直检查,什么都没发现,再水平搜索,发现一个我们感兴趣的节点(淡紫色),因为它携带 forced neighbor ,将这个淡紫色节点加入 openlist 。因为还没达到对角搜索终止的两个条件,所以继续向对角方向移动,什么都没发现,直到到达地图边界,结束此次对角线搜索。将图 4 中的黄色节点加入 closelist ,此时 openlist 中只有淡紫色节点。
-
图 5:从 openlist 中弹出黄色节点(图 4 的淡紫色节点),沿着之前的方向(灰色-> 黄色)开始搜索,水平向右直到到达地图边界。正常来说,一次标准的直线运动到这应该结束了,但是这个黄色节点有一个 forced neighbor ,所以还必须向这个 forced neighbor 的方向进行搜索。
-
图 6:按照对角线运动的规则,沿着当前节点(黄色) -> 其 forced neighbor 的方向搜索。
-
图 7:竖直方向、水平方向均没有发现,继续对角移动。
-
图 8:按照之前的方向继续对角运动,向水平方向搜索时碰到边界,竖直方向搜索时发现目标节点,发现目标节点等同于发现了一个携带 forced neighbor 的节点,因此将当前节点(紫色)加入 openlist ,并结束这次对角搜索。
-
图 9:弹出当前 openlist 唯一的节点(黄色),按照之前的方向对角运动,水平方向搜索碰到边界,竖直方向搜索时发现目标节点,将目标节点加入 openlist ,继续对角搜索时到达地图边界,结束此次对角搜索。
-
图 10:弹出当前 openlist 唯一的节点(红色),发现是目标节点,因此完成整条路径的搜索,通过回溯找到最短路径。
JPS搜索过程总结
- 第一步,确定起始点和终点,并将起始点放入open list中。
- 第二步,从open list中取出最新点,分别按照水平或对角方向移动,每次移动一次就观察一下此时点的水平或者对象线上有没有跳跃点或者终点。
- 第三步,有终点就将终点放入open list,并停止搜索,将终点的父节点设置为上一个从open list中取出的点,无终点的话,记录下跳跃点以及其对应的邻居,计算各个跳跃点的代价,将代价最小的跳跃点放入open list。
- 第四步,从open list中取出最新的点,然后继续重复第二步、第三步,直到搜索到终点或者无解时停止。
1.使用JPS算法比A更快(绝大部分地图),内存占用更小,因为openlist少了很多节点(最差的情况和A一样,最差的是每个障碍都不连续,中间都有缝隙,这样所有地方都是跳点了)。
2.只适用于网格节点类型,不支持Navmesh或者路径点寻路方式。
以上是关于A*与JPS算法的背景和实现的主要内容,如果未能解决你的问题,请参考以下文章