最短路径

最短路径搜寻之A*算法

2018-12-08  本文已影响0人  南湖Giser

声明

    本文译自 Patrick Lester先生的一片博文,觉着实在是一片优秀的文章,于是打算花点时间将其翻译成中文,一来自己学习一番,二来可以方便国内读者。如有谬误,欢迎拍砖。原文请参见: https://www.gamedev.net/articles/programming/artificial-intelligence/a-pathfinding-for-beginners-r2003/

    A-Start 算法对于初学者来说可能有些复杂,尽管网上有很多讲解它的文章,但大多是在认为读者已对该算法有一定程度了解的基础上写的。这篇博文则主要面向入门小白。
    这篇文章不是我们学习A*算法的终点,而是我们阅读其他相关内容的基础,因此在文末我列出了一些优秀的博文以供大家进一步学习。
    最后我还得声明一点,这篇文章并不是面向程序设计的,你可以将本文的观点运用到任何编程语言当中。我也在文末给出了一个例子程序的链接,你也许会用得上。这个例子包含C++和Basic两个版本,当然也包含了可执行文件在里面。

介绍:搜索区

    假设有个人想从A点到达B点,A、B之间有一堵墙阻隔。如下图所示,绿色的方框代表A点,红色的方框代表B点,中间蓝色的条状物则代表这堵墙。 图1

    你应该已经发现,我们需要做的第一件事就是把搜索区与分成一个一个的网格,即搜索区简化。通过这个方法将我们的搜索区域简化成了一个二维数组,数组中的每个元素都代表一个网格,并且记录了状态为可达或不可达。在路径搜寻的过程中,逐步判断下一步应当踏入哪个网格,直到达到目标点。
    我们把每个网格的中心点叫做“节点”,当参阅其他一些关于路径搜寻的文章时,你会发现他们也是称“节点”。为什么不直接叫网格呢?因为除了正方形格网外我们还可将搜索去分成其他不同形状的多边形,比如矩形、正六边形、三角形甚至其他任意形状。所以我们在这些多边形上选取一个代表点(几何中心、重心等)来代表多边形本身,统一称为“节点”。

开始搜索

    在上面的讨论中,我们将搜索区简化成了一个个网格,下一步就是在此基础上搜索最短路径。从A点开始,向外搜索相邻网格知道到达目标点。
    1.从A点开始,将其添加到待考虑的“open list”中,"open list"就像一张购物清单,现在单子里只有一件物品,但稍后我们会往里加入更多。里面包含了到目标点的最短路径沿途可能会经过的网格,因此需要我们逐个检查判别。
    2.考察与起点A相邻的所有网格,代表墙、水域或其他非法区域(不可达区域)的直接略过,可达的网格加入“open list”,对于所有选中的相邻的网格,同时记录网格A为其"父亲网格",这对于我们追踪路径十分重要,后面我们将详细讨论。
    3.把网格A从"open list"中去掉,将其加入"closed list"中,从现在开始就不需要再考虑它了。
    到这一步你就进行到了如下图所示的一个状态。中间绿色的网格代表你的起点,起点网格的边缘以亮绿色高亮显示表示该网格已经加入到"closed list"。周围所有相邻的网格都被加入到了"open list"等待检核,并且每个网格里都有一个个灰色的指针标记指向该网格的“父亲网格”,也就是我们的起点网格A。


图2

    下一步我们将从"open list"中选取一个F值最小的网格,因此如何计算F值是我们接下来要讨论的内容。

路径评分

    路径构建过程中网格选取的关键就是如何计算下面这个等式:F=G+H

继续搜索

    在路径搜索过程中,我们从"open list"中选出F值最小的网格,然后进行下面的步骤:
    最初的9个网格中,在起始网格A(绿色)被移至"closed list"后,"open list"中就还剩下周围的8个网格,从图4我们会发现,紧接着右边的那个网格就是F最小(40)的网格,所以选择这个格网作为下一步位置。对应图中边缘高亮显示的网格。


图4

    首先将选出的最小F值的网格从"open list"中移除并加入到"closed list"(这就是为什么该网格边缘现在是高亮显示的),该网格记为node1,然后开始检核node1接下周围的8个网格再次选出F值最小的一个网格。右边的网格代表的是墙体所以忽略掉,左边的绿色网格(即我们的起始网格)由于在前面的步骤中已经加入到"closed list",所以也不予考虑。剩下的四个网格已经加入的"open list",接下来我们考察经由网格node1到达"open list"中的网格的路径长度是否比不经过node1的路径短。以node1正上方的网格为例,如果我们不经由node1直接从网格A到达该点,那么G值为14,如果经过了node1,G值为20(在node1 G值为10的基础上再加上垂直向上移动的耗费10),20>14所以选择不经过node1直接从A对角到达node1正上方的网格。对"open list"中的4个网格执行同样的判断,我们发现都不会因为经过node1而使得路径变短。所以将直接考察这4个网格以从中选出下一步将到达的位置。
    去除node1后"open list"还剩下7个考察对象,遍历"open list"获取F最小的那个网格,在给出的例子中,F最小值为54,并且有两个网格都是54。选哪个呢?这不是那么重要。处于算法的速度考虑,我们应该选择相对后加入到"open list"中那个网格,这会使得我们的搜索算法在一步步接近终点的搜索过程中更偏向于选择最新发现的网格。但这确实不是那么重要(这就是为什么两个不同版本的A-Start算法会得到不同的最短路径长度)。因此我们选择起始网格A右下方的网格,如下图所示,同样边缘已高亮显示。


图5
    当前我们处于node2这个位置,考察node2邻接的网格发现,右边格网代表隔墙,所以不予考虑。node2右下方的网格也不考虑,因为我们认为有墙体的拐角存在二导致无法直接到达,只能通过下移然后再向右移动一个网格到达。(注意:这里的拐角是否可直接到达你根据你的应用场景二定的,并不是说这种情况一定不可达)。
    这么一来,node2周围就剩下5个网格待考,底下的两个还没有加入到"open list",所以把它两加进去并且记录node2作为它们的父亲节点。剩下的三个网格中,有两个(绿色起点网格和它左边的网格都已经加入到"closed list"),因此也忽略掉。剩下的最后一个网格(node2左边的这个)我们将考察如果路径经过node2再到达该网格是否会获得更低的耗费,答案是否定的,所以我们继续考察"open list"中剩下的网格。重复这个步骤知道将重点网格加入到"closed list"。最终就是如下图所示的状态。
图6

    注意:如上图所示起始网格以下的第二个网格的个指标值发生了变化,在此之前G值为28,父亲节点为它右上方的网格,现在它的父亲节点为正上方的网格。在路径搜索过程中,当我们发现有一条更低耗费的路径存在时,该网格的父亲节点就会更新并且重新计算各指标值。但是这个改变在这个例子中看起来也不是那么重要,因为整个过程中有那么多的最佳道路的调整选择二导致了许多不同的解决方案。
    所以我们是如何获取到这条路径的呢?简单,从红色的目标网格开始,根据记录的父亲节点逐个往回取,和箭头的指示方向是一致的。这样就回到了起始的网格A,同时也得到了我们的最短路径链。如下面的插图所示,A移动到B的路径搜寻说白了就是这么简单的一件事:从一个网格的中心点移动到下一个网格的中心点,直到找到你希望到达的目标点为止。


图7

算法总结

    到这里你已经对整个算法有了大概的了解,现在让我们逐步罗列出算法过程:
    1.将当前网格currentNode从open list中删除并加入到closed list;
    2.检查currentNode周围的网格,对于已加入到closed list的和现实中不可达的网格(如墙体、水域或其他非法区)直接忽略,对于剩下的并且尚未加入到open list的网格,我们将其加入到open list,同时记录currentNode为它们的父亲网格;
    3.对于剩下的并且已经加入到open list的与currentNode相邻的网格记为neighborNode,考虑从起始点到该neighborNode是否还存在一条更佳的路径。换句话说,如果我们选择经由currentNode到达neighborNode是否会使得neighborNode的G值变得更小,如果不会,那啥也不用做;如果G值可变得更小,就改变neighborNode的父亲节点为currentNode,最后计算出neighborNode新的F值和G值并更新记录。
    4.接着步骤2说,尚未加入到open list的网格我们会将其加入进去(当然也可能是算法启动时初次加入起始点网格);
    5.重复下面的步骤:
        a)获取到open list中最小F值对应的网格,将其作为新的currentNode
        b)将currentNode移到closed List
        c)对于其周围相邻的8个网格......
             - 如果网格不可达或者已经加入到closed list,忽略,否则执行下面的步骤
             - 如果网格不在open list中,则加入到open list,记录当前网格currentNode为其父亲节点,计算出F值、G值和H值并记录。
             - 如果已经加入到open list中,以G为指标考察是否存在更佳路径,G值越低我们则认为路径约佳。如果经由currentNode达到这个相邻网格的路径耗费更低(即路径更佳),那么就需更改这个相邻网格的付清节点为currentNode并重新计算F值和G值。如果你的open list是按F值排序的,那么还需要进行重排序。
        d)出现下列情况之一则算法终止:
             - 终点网格已加入到closed list,表示已找到最佳路径,现在我们需要把这条路径保存下来。从终点网格开始,根据记录的父亲节点往回逐步递推直到起始网格,沿途经过的网格即是我们所求的路径了;
             - 还没有移动到终点网格但open list已经空了,那就表示无解,算法终止;
(注意:在这篇文章的早期版本中我们建议当目标网格被加入到open list时即终止算法,而不是等到加入到closed list才终止,这样可以提升算法效率并且在大多数时候会得到最短路径解。但也并不总是这样,比如如果有条河流处于倒数第二个网格和最后一个网格之间时,它们之间的移动耗费可能发生很大变化)

题外话

    值得一提的是,当你在网上或者一些论坛读到关于A-Star的文章时,你可能会发现有些人把一些并不是A-Star算法的代码也称作是A-Start。提到A-Star算法,那一定包含我们在上面提到的这些元素,尤其是"open list"、"closed list"以及F、G、H的评分机制。当然还有许多其他的路径搜索算法,但它们都不是A-Star,A-Star的算法性能总体上来说更为优秀,在本文末给出的一片参考文章中,Bryan Stout先生讨论了很多路径寻找的算法的原理以及它们的优缺点。其实这些算法无孰优孰劣之分,都有其适应的场景,所以你在应用相应的算法之前必须搞清楚你的应用场景。好啦,回到正文......

算法实现的注意事项

    现在你已经弄清楚了算法的基本原理,但是在算法实现的过程中还有一些需要注意的点。下面这些观点虽然是针对C++和Blitz Basic的,但对于其他程序设计语言我想也同样适用。
    1.避免碰撞
    如果你仔细看过我的代码就会发现,屏幕上的其他单元都是被忽略掉的,这些单元之间可相互穿行。是否考虑这些单元取决与你的游戏设计。如果你打算在算法中考虑这些单元并让他们相互移动,那么我建议你只考虑在计算路径时已停止或与寻路单元相邻的单位,把它们代表的当前位置视为不可达。对于正在移动的相邻单元,你可以通过惩罚位于其各自路径上的节点来阻止冲突,从而促使寻路单元找到备用路径。
    如果你选择考虑那些移动着非相邻单元,那么需要一个方法来预测在给定时间点该单元的位置,这样才能准确避开它。否则你可能得到的是一条为了避开一些实际并不存在的单元而形成的弯弯曲曲的路径。
    当然了,你还需要碰撞检测代码,因为无论当前计算出的路径多么优质,它都是会随着时间改变的。当一个单元发生碰撞时,就会重新计算路径,如果另一个单元正在移动而不是正面碰撞,则在继续当前路径之前等待另一个单元走开。
    上面这些建议也许已经足够你开始A-Star之旅了,如果你想了解更多,下面这些链接倒是很有用哦。

上一篇 下一篇

猜你喜欢

热点阅读