A*算法个人见解以及后期路径优化思路(精)

致谢

转载至:https://blog.csdn.net/h348592532/article/details/44421753?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase

https://blog.csdn.net/qq_36946274/article/details/81982691

https://blog.csdn.net/weixin_44489823/article/details/89382502

https://www.cnblogs.com/billyrun/articles/5498802.html

A*算法与Dijkstra算法、最佳优先搜索BFS

Dijkstra算法从物体所在的初始点开始,访问图中的结点。它迭代检查待检查结点集中的结点,并把和该结点最靠近的尚未检查的结点加入待检查结点集。该结点集从初始结点向外扩展,直到到达目标结点。Dijkstra算法保证能找到一条从初始点到目标点的最短路径,只要所有的边都有一个非负的代价值。(我说“最短路径”是因为经常会出现许多差不多短的路径。)在下图中,粉红色的结点是初始结点,蓝色的是目标点,而类菱形的有色区域(注:原文是teal areas)则是Dijkstra算法扫描过的区域。颜色最淡的区域是那些离初始点最远的,因而形成探测过程(exploration)的边境(frontier):

在这里插入图片描述
最佳优先搜索(BFS)算法按照类似的流程运行,不同的是它能够评估(称为启发式的)任意结点到目标点的代价。与选择离初始结点最近的结点不同的是,它选择离目标最近的结点。BFS不能保证找到一条最短路径。然而,它比Dijkstra算法快的多,因为它用了一个启发式函数(heuristic function)快速地导向目标结点。例如,如果目标位于出发点的南方,BFS将趋向于导向南方的路径。在下面的图中,越黄的结点代表越高的启发式值(移动到目标的代价高),而越黑的结点代表越低的启发式值(移动到目标的代价低)。这表明了与Dijkstra 算法相比,BFS运行得更快。
在这里插入图片描述
然而,这两个例子都仅仅是最简单的情况——地图中没有障碍物,最短路径是直线的。现在我们来考虑前边描述的凹型障碍物。Dijkstra算法运行得较慢,但确实能保证找到一条最短路径:
在这里插入图片描述
另一方面,BFS运行得较快,但是它找到的路径明显不是一条好的路径:
在这里插入图片描述
问题在于BFS是基于贪心策略的,它试图向目标移动尽管这不是正确的路径。由于它仅仅考虑到达目标的代价,而忽略了当前已花费的代价,于是尽管路径变得很长,它仍然继续走下去。

结合两者的优点不是更好吗?1968年发明的A算法就是把启发式方法(heuristic approaches)如BFS,和常规方法如Dijsktra算法结合在一起的算法。有点不同的是,类似BFS的启发式方法经常给出一个近似解而不是保证最佳解。然而,尽管A基于无法保证最佳解的启发式方法,A*却能保证找到一条最短路径。

A*算法

我将集中讨论A算法。A是路径搜索中最受欢迎的选择,因为它相当灵活,并且能用于多种多样的情形之中。

和其它的图搜索算法一样,A潜在地搜索图中一个很大的区域。和Dijkstra一样,A能用于搜索最短路径。和BFS一样,A能用启发式函数(注:原文为heuristic)引导它自己。在简单的情况中,它和BFS一样快。
在这里插入图片描述
在凹型障碍物的例子中,A
找到一条和Dijkstra算法一样好的路径:
在这里插入图片描述
成功的秘决在于,它把Dijkstra算法(靠近初始点的结点)和BFS算法(靠近目标点的结点)的信息块结合起来。在讨论A的标准术语中,
g(n)表示从初始结点到任意结点n的代价
h(n)表示从结点n到目标点的启发式评估代价

在上图中,yellow(h)表示远离目标的结点而teal(g)表示远离初始点的结点。当从初始点向目标点移动时,A
权衡这两者。每次进行主循环时,它检查f(n)最小的结点n,其中f(n) = g(n) + h(n)。

三种启发函数(前两种用于网格)此次A*你直接用曼哈顿来看

1.曼哈顿(也就是只能走直角)
如果图形中只允许朝上下左右四个方向移动,则启发函数可以使用曼哈顿距离,它的计算方法如下图所示:
在这里插入图片描述
计算曼哈顿距离的函数如下,这里的D是指两个相邻节点之间的移动代价,通常是一个固定的常数。

function heuristic(node) =
dx = abs(node.x - goal.x)
dy = abs(node.y - goal.y)
return D * (dx + dy)

2.对角线(能走对角线)
如果图形中允许斜着朝邻近的节点移动,则启发函数可以使用对角距离。它的计算方法如下:
在这里插入图片描述
计算对角距离的函数如下,这里的D2指的是两个斜着相邻节点之间的移动代价。如果所有节点都正方形,则其值就是:
function heuristic(node) =
dx = abs(node.x - goal.x)
dy = abs(node.y - goal.y)
return D * (dx + dy) + (D2 - 2 * D) * min(dx, dy)

3.欧几里得(直接两点直线)
如果图形中允许朝任意方向移动,则可以使用欧几里得距离。

欧几里得距离是指两个节点之间的直线距离,因此其计算方法也是我们比较熟悉的:
其函数表示如下:

function heuristic(node) =
dx = abs(node.x - goal.x)
dy = abs(node.y - goal.y)
return D * sqrt(dx * dx + dy * dy)

搜索区域(The Search Area)

我们假设某人要从 A 点移动到 B 点,但是这两点之间被一堵墙隔开。如图 1 ,绿色是 A ,红色是 B ,中间蓝色是墙。
在这里插入图片描述
你应该注意到了,我们把要搜寻的区域划分成了正方形的格子。这是寻路的第一步,简化搜索区域,就像我们这里做的一样。这个特殊的方法把我们的搜索区域简化为了 2 维数组。数组的每一项代表一个格子,它的状态就是可走 (walkalbe) 和不可走 (unwalkable) 。通过计算出从 A 到 B需要走过哪些方格,就找到了路径。一旦路径找到了,人物便从一个方格的中心移动到另一个方格的中心,直至到达目的地。

方格的中心点我们成为“节点 (nodes) ”。如果你读过其他关于 A* 寻路算法的文章,你会发现人们常常都在讨论节点。为什么不直接描述为方格呢?因为我们有可能把搜索区域划为为其他多变形而不是正方形,例如可以是六边形,矩形,甚至可以是任意多变形。而节点可以放在任意多边形里面,可以放在多变形的中心,也可以放在多边形的边上。我们使用这个系统,因为它最简单。

开始搜索(Starting the Search)

一旦我们把搜寻区域简化为一组可以量化的节点后,就像上面做的一样,我们下一步要做的便是查找最短路径。在 A* 中,我们从起点开始,检查其相邻的方格,然后向四周扩展,直至找到目标。

我们这样开始我们的寻路旅途:

从起点 A 开始,并把它就加入到一个由方格组成的 open list( 开放列表 ) 中。这个 open list 有点像是一个购物单。当然现在 open list 里只有一项,它就是起点 A ,后面会慢慢加入更多的项。 Open list 里的格子是路径可能会是沿途经过的,也有可能不经过。基本上 open list 是一个待检查的方格列表。

查看与起点 A 相邻的方格 ( 忽略其中墙壁所占领的方格,河流所占领的方格及其他非法地形占领的方格 ) ,把其中可走的 (walkable) 或可到达的 (reachable) 方格也加入到 open list 中。把起点 A 设置为这些方格的父亲 (parent node 或 parent square) 。当我们在追踪路径时,这些父节点的内容是很重要的。稍后解释。

把 A 从 open list 中移除,加入到 close list( 封闭列表 ) 中, close list 中的每个方格都是现在不需要再关注的。

如下图所示,深绿色的方格为起点,它的外框是亮蓝色,表示该方格被加入到了 close list 。与它相邻的黑色方格是需要被检查的,他们的外框是亮绿色。每个黑方格都有一个灰色的指针指向他们的父节点,这里是起点 A 。
在这里插入图片描述
下一步,我们需要从 open list 中选一个与起点 A 相邻的方格,按下面描述的一样或多或少的重复前面的步骤。但是到底选择哪个方格好呢?具有最小 F 值的那个

路径排序(Path Sorting)

计算出组成路径的方格的关键是下面这个等式:

F = G + H

这里,

G = 从起点 A 移动到指定方格的移动代价,沿着到达该方格而生成的路径。

H = 从指定的方格移动到终点 B 的估算成本。
这个通常被称为试探法,有点让人混淆。为什么这么叫呢,因为这是个猜测。直到我们找到了路径我们才会知道真正的距离,因为途中有各种各样的东西 ( 比如墙壁,水等 ) 。本教程将教你一种计算 H 的方法,你也可以在网上找到其他方法。

我们的路径是这么产生的:反复遍历 open list ,选择 F 值最小的方格。这个过程稍后详细描述。我们还是先看看怎么去计算上面的等式。(在后面我会提及一些优化,不必要全部遍历open list,每一次都sort会极大地消耗时间)

如上所述, G 是从起点A移动到指定方格的移动代价。在本例中,横向和纵向的移动代价为 10 ,对角线的移动代价为 14 。之所以使用这些数据,是因为实际的对角移动距离是 2 的平方根,或者是近似的 1.414 倍的横向或纵向移动代价。使用 10 和 14 就是为了简单起见。比例是对的,我们避免了开放和小数的计算。这并不是我们没有这个能力或是不喜欢数学。使用这些数字也可以使计算机更快。稍后你便会发现,如果不使用这些技巧,寻路算法将很慢。

既然我们是沿着到达指定方格的路径来计算 G 值,那么计算出该方格的 G 值的方法就是找出其父亲的 G 值,然后按父亲是直线方向还是斜线方向加上 10 或 14 。随着我们离开起点而得到更多的方格,这个方法会变得更加明朗。

有很多方法可以估算 H 值。这里我们使用 Manhattan 方法,计算从当前方格横向或纵向移动到达目标所经过的方格数,忽略对角移动,然后把总数乘以 10 。之所以叫做 Manhattan 方法,是因为这很像统计从一个地点到另一个地点所穿过的街区数,而你不能斜向穿过街区。重要的是,计算 H 是,要忽略路径中的障碍物。这是对剩余距离的估算值,而不是实际值,因此才称为试探法。

把 G 和 H 相加便得到 F 。我们第一步的结果如下图所示。每个方格都标上了 F , G , H 的值,就像起点右边的方格那样,左上角是 F ,左下角是 G ,右下角是 H 。
在这里插入图片描述
好,现在让我们看看其中的一些方格。在标有字母的方格, G = 10 。这是因为水平方向从起点到那里只有一个方格的距离。与起点直接相邻的上方,下方,左方的方格的 G 值都是 10 ,对角线的方格 G 值都是 14 。

H 值通过估算起点于终点 ( 红色方格 ) 的 Manhattan 距离得到,仅作横向和纵向移动,并且忽略沿途的墙壁。使用这种方式,起点右边的方格到终点有 3 个方格的距离,因此 H = 30 。这个方格上方的方格到终点有 4 个方格的距离 ( 注意只计算横向和纵向距离 ) ,因此 H = 40 。对于其他的方格,你可以用同样的方法知道 H 值是如何得来的。

每个方格的 F 值,再说一次,直接把 G 值和 H 值相加就可以了。

继续搜索(Continuing the Search)

为了继续搜索,我们从 open list 中选择 F 值最小的 ( 方格 ) 节点,然后对所选择的方格作如下操作:

把它从 open list 里取出,放到 close list 中。

检查所有与它相邻的方格,忽略其中在 close list 中或是不可走 (unwalkable) 的方格 ( 比如墙,水,或是其他非法地形 ) ,如果方格不在open lsit 中,则把它们加入到 open list 中。

把我们选定的方格设置为这些新加入的方格的父亲。

如果某个相邻的方格已经在 open list 中,则检查这条路径是否更优,也就是说经由当前方格 ( 我们选中的方格 ) 到达那个方格是否具有更小的 G 值。如果没有,不做任何操作。

相反,如果 G 值更小,则把那个方格的父亲设为当前方格 ( 我们选中的方格 ) ,然后重新计算那个方格的 F 值和 G 值。如果你还是很混淆,请参考下图。

在这里插入图片描述
Ok ,让我们看看它是怎么工作的。在我们最初的 9 个方格中,还有 8 个在 open list 中,起点被放入了 close list 中。在这些方格中,起点右边的格子的 F 值 40 最小,因此我们选择这个方格作为下一个要处理的方格。它的外框用蓝线打亮。

首先,我们把它从 open list 移到 close list 中 ( 这就是为什么用蓝线打亮的原因了 ) 。然后我们检查与它相邻的方格。它右边的方格是墙壁,我们忽略。它左边的方格是起点,在 close list 中,我们也忽略。其他 4 个相邻的方格均在 open list 中,我们需要检查经由这个方格到达那里的路径是否更好,使用 G 值来判定。让我们看看上面的方格。它现在的 G 值为 14 。如果我们经由当前方格到达那里, G 值将会为 20(其中 10 为到达当前方格的 G 值,此外还要加上从当前方格纵向移动到上面方格的 G 值 10) 。显然 20 比 14 大,因此这不是最优的路径。如果你看图你就会明白。直接从起点沿对角线移动到那个方格比先横向移动再纵向移动要好。

当把 4 个已经在 open list 中的相邻方格都检查后,没有发现经由当前方格的更好路径,因此我们不做任何改变。现在我们已经检查了当前方格的所有相邻的方格,并也对他们作了处理,是时候选择下一个待处理的方格了。

因此再次遍历我们的 open list ,现在它只有 7 个方格了,我们需要选择 F 值最小的那个。有趣的是,这次有两个方格的 F 值都 54 ,选哪个呢?没什么关系。从速度上考虑,选择最后加入 open list 的方格更快。这导致了在寻路过程中,当靠近目标时,优先使用新找到的方格的偏好。但是这并不重要。 ( 对相同数据的不同对待,导致两中版本的 A* 找到等长的不同路径 ) 。

我们选择起点右下方的方格,如下图所示。
在这里插入图片描述
这次,当我们检查相邻的方格时,我们发现它右边的方格是墙,忽略之。上面的也一样。

我们把墙下面的一格也忽略掉。为什么?因为如果不穿越墙角的话,你不能直接从当前方格移动到那个方格。你需要先往下走,然后再移动到那个方格,这样来绕过墙角。 ( 注意:穿越墙角的规则是可选的,依赖于你的节点是怎么放置的 )但是在具体实现代码过程中,墙角下的格子忽略判断会比较麻烦,所以我就懒得忽略了,直接加上了,和图中描述有些许出入,但是不影响算法理解和整体算法结构

这样还剩下 5 个相邻的方格。当前方格下面的 2 个方格还没有加入 open list ,所以把它们加入,同时把当前方格设为他们的父亲。在剩下的3 个方格中,有 2 个已经在 close list 中 ( 一个是起点,一个是当前方格上面的方格,外框被加亮的 ) ,我们忽略它们。
最后一个方格,也就是当前方格左边的方格,我们检查经由当前方格到达那里是否具有更小的 G 值,这里并没有更小的G值。
(有许多文章中都没有提及到这一点,会导致算法的有问题)
因此我们准备从 open list 中选择下一个待处理的方格。

不断重复这个过程,直到把终点也加入到了 open list 中,此时如下图所示。

在这里插入图片描述
这里图又出现了一个问题,我们上面说到把结点从open list放到close list中会把当前结点标蓝。
在进入F值为74的时候,我们发现会有三个值为74的结点在openlist中,当值都一样的时候我们为了节省时间效率,会选取尽量靠最后加入openlist的结点,所以我们会选择具有三个黄色框框的结点。在接下来的扩张中,我们会计算出很多个F值74的结点。所以按选取顺序来的,起点的左上角和左下角的另两个点是选取不到的不会进入到closelist中。也就是不能有蓝色框。
我为什么强调这点呢,因为在学习这个算法的时候,我一直在半理解的状态下,卡了很久,为什么那两个点会有蓝色框。

注意,在起点下面 2 格的方格的父亲已经与前面不同了。之前它的 G 值是 28 并且指向它右上方的方格。现在它的 G 值为 20 ,并且指向它正上方的方格。这在寻路过程中的某处发生,使用新路径时 G 值经过检查并且变得更低,因此父节点被重新设置, G 和 F 值被重新计算。尽管这一变化在本例中并不重要,但是在很多场合中,这种变化会导致寻路结果的巨大变化。

那么我们怎么样去确定实际路径呢?很简单,从终点开始,按着箭头向父节点移动,这样你就被带回到了起点,这就是你的路径。如下图所示。从起点 A 移动到终点 B 就是简单从路径上的一个方格的中心移动到另一个方格的中心,直至目标。就是这么简单!
结果图仅供参考,不同的启发函数(曼哈顿、对角线)对应不同路线
路线怎么出来的?用了Dijkstra算法思想的回溯法
我们不是记录了每个节点的父节点吗,直接从终点选取各自父节点回溯到起点
在这里插入图片描述

A算法总结(Summary of the A Method)

Ok ,现在你已经看完了整个的介绍,现在我们把所有步骤放在一起:

把起点加入 open list 。

重复如下过程:

a. 遍历 open list ,查找 F 值最小的节点,把它作为当前要处理的节点。

b. 把这个节点移到 close list 。

c. 对当前方格的 8 个相邻方格的每一个方格?

◆ 如果它是不可抵达的或者它在 close list 中,忽略它。否则,做如下操作。

◆ 如果它不在 open list 中,把它加入 open list ,并且把当前方格设置为它的父亲,记录该方格的 F , G 和 H 值。

◆ 如果它已经在 open list 中,检查这条路径 ( 即经由当前方格到达它那里 ) 是否更好,用 G 值作参考。更小的 G 值表示这是更好的路径。如果是这样,把它的父亲设置为当前方格,并重新计算它的 G 和 F 值。如果你的 open list 是按 F 值排序的话,改变后你可能需要重新排序。

d. 停止,当你

◆ 把终点加入到了 open list 中,此时路径已经找到了,或者

◆ 查找终点失败,并且 open list 是空的,此时没有路径。

保存路径。从终点开始,每个方格沿着父节点移动直至起点,这就是你的路径。

A*算法及其衍生

A算法的思想上面讲的很清楚很详细了,我们可以发现,A算法求的是两个静态点的短时间短路径的算法。可以用于游戏、机器人路线等求最短路径。但是现实生活中,我们很多时候会遇到动态点,当有其他点来干预的时候,A算法是不能满足的。这里我们ARA、D*、Field D*、Block A*等,有其他情况的介绍,有兴趣可以自己去了解。

路径优化思路(精)

有其他作者手动用cocos写了一个A星搜索的例子
地图大小是128*70 = 9000左右格子
当地图完全随机时(所有格子的80%可以走,20%不能走)
沿对角线从左下角走到右上角寻路时间大概是0.05~0.07
这种时间消耗个人感觉还是比较理想的
在这里插入图片描述
主循环286次,用时0.046秒,路径长度131步
这里可以看出一个问题,路线并不是一条对角线!
主要因为计算F值时,我使H=abs(x-x)+abs(y-y),避免了使用平方开方耗时
当然如果在游戏中,允许斜线走格子时,H值要算的更准确
然而之后我在当前的地图上
又竖起了六道大墙
来测试复杂地形对A星带来的影响
在这里插入图片描述
如图所示
AStarMap::findPath over path found! 5686
HelloWorld::drawPath steps = 304

这时A星需要遍历5600多次(满屏9000个格子约有7000个白格子可以走,也就是说遍历的上限是7000次)
路径长度也增加了一倍
最初我的A星算法需要消耗2.6秒左右
最后再我的不断调整下缩短到0.5秒以内(还可以继续优化)
下面分享一下 优化的策略

1.降低循环消耗
A星每一次遍历需要拿到open集中的最优节点
因此首先想到的会是对open集合排序
在这个前提下
5600多次的排序,而且有时open集中的元素多达上百个
显然是非常耗时的
我所用的open集合是一个vector(十分无脑)
一个简单粗暴的方法可以大幅提升效率
就是放弃std::sort
因为std::sort把整个数组都sort了
我们其实只需要拎出来最优的那一个
因此自己写一个查找,一趟下来找到最优的,查到open集最前面!
只要理解了A星的意义,不难想出这个方法
这个方法有一个缺点,就是仍然做了5600多次查找!
如果继续使用sort而减少sort的次数
也能一定程度上提升效率
最简单的例子
当open集合sort一次之后
下一次遍历邻节点时
若邻节点的F值比当前open[0]的还好
那么直接插它前面!
这样这一次循环后,我们其实无需sort
因为我们知道(插过之后的)open[0]就是当前最好的节点
另一种情况
此次循环的邻节点都没有open[0]好
那么全部push_back
此时,仍然open[0]是当前最好的节点,还是可以不sort
基于这种思路,我使用了记数的办法
记录每时每刻从首元素开始,有序元素的长度
当有序元素长度==0时才sort
这样减少了80%的sort次数
效率提升翻倍
如果更进一步
判断open集的长度,仅对前十元素排序
动态的比较新节点的价值是否进入前十
并维护前十元素的有序性(只sort前十,小sort)
当前十一个个用完了也没补上的时候,再来一个大sort
当然这个思路实现起来比较麻烦
所以我也没去实现

2.设计数据结构
深入的优化必然需要考虑选择更合适的数据结构
普通vector局限性很强
sort浪费时间
然而如果使每一个新节点加入open时有序插入
那么sort可以省略了
这里看了其他大牛的经验
使用二元堆/二叉堆的思路非常好
这样的进一步优化十分高效

3.用空间换时间
A星其中一步需要检查新发现的邻居节点是否在open中已经有了
最初我的做法是遍历open表
后来我使用的一个新的二维数组来存放open(open中的元素存两遍,vector中的用于查找最优,二维数组中检测是否存在)
这样很纯的方法,证明也确实能提高一点效率
因为如果每次新节点都要去遍历open表(大部分结果是未找到)
十分无意义,浪费时间
这种空间换时间的策略
还可以用于H值的计算
因为H值是一定的,一次计算后并保存
省去以后每次遍历邻节点时的重复计算

4.人工智能与人类智能结合的究极优化
以上策略都是优化A星本身
然而我们只能提升每一步算法的效率
面对复杂地图,A星还是会犯错误
例如加了6面墙的地图
无论如何优化设计F值,我的A星都需要遍历5600次左右
即使我已经优化到了0.5秒以内
与没有墙时的286次循环耗时0.05秒依然有着10倍的差距
这时最好的策略就是用人脑来帮一帮A星
假设在游戏中有相似的情况
阻挡路线的是河而不是墙
若要过河必须走桥否则必然碰壁
那么可以把整张大地图划分为多个区域
在区域内的寻路必然超快,因为没有大的阻挡,路线也短
跨区域的寻路
可以使用拆分策略
例如假设两区域间必然要过桥
那么就分别用A星计算

起点~桥

桥~终点

这两段路程
这样A星省去绝大部分错误的尝试
可以大幅提升效率

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值