A*算法之旅之A*算法实现(二)

A*算法之旅之A*算法实现(二)


序言:建议刚看这篇文章的朋友大致看一下该系列的前一篇:A*算法之旅之初识A*算法(一)。这一篇文章是继承上一篇而来,文中部分说明沿用自上一篇,依旧以游戏寻路为例。

很好的A*入门文章–>A* Pathfinding for Beginer ,简便起见,本篇中使用该文章的部分图片。
其中文译文–>A* Pathfinding for Beginer 译文 ,有部分指代不明处,不太明白的往下看。


本节辅以图片形式进行说明

该例中,我们假定我们从绿格子->start 出发,目标是到达红格子->end,寻找这样一条路径,同时在该例演示中,我们假定中间有障碍物–>蓝格子。

aStarT1

分析

首先我们要看到的是整个搜索空间/搜索区域。整个搜索空间被我们分成许多小的正方格,而这也是问题的简化。而实际情况下,根据需求我们可能需要各种不同的区域划分方法,我们可能划分成正六边形,三角形等。
本例我们用正四边形将其划分,且节点位于正方形中心,这样二维空间可以用二维数组来模拟。整个二维空间所有的格子均有一个标记,说明是 可通过的->路 或者是 不可通过的->墙 。划分完毕后开始搜索。

开始搜索

沿用之前给出的不完备的伪代码

start 加入 一个空队列中
while(队列不为空)
    考察队列中最优的元素;
    将该元素移出队列;
    添加元素邻近元素入队列;
    if(队列中有end)
        路径找到;结束;
未找到路径;结束;

我们将上述伪代码具体化,之前未提及的随后会说明:

添加 start 进 open_list
while(open_list 不为空)
    // 开始是current = start
    取出open_list中最优元素, // f(n)最小元素
    赋值给 current ;        // "当前"节点
    将current从open_list中移除;
    添加 current 进 close_list;

    考察current的所有邻居neighbor,
        if 该neighbor是"可通过的" 且 没有被加入close_list
            if 该neighbor没有在open_list中
                计算其 f(n),g(n),h(n);
                将其加入open_list;
            if 该neighbor已经在open_list中
                通过分析,对其g(n),f(n)进行调整 
                // h(n)一般是固定值

    所有neighbor节点考察完后,if( end节点 在 open_list中)
        找到路径;结束;
未找到路径,结束;

首先:我们将开始节点start 放入一个队列中,这个队列我们称之为 开启列表”open list” (或其他的数据结构,不同结构的选取会影响速度,稍后会谈论这个问题,此处知道不一定是队列即可)
之后就进入主循环:
while(open_list 不为空) ,当open_list中还有元素时,我们就做如下操作:

  1. 从 open_list 中根据 f(n)的值 大小找出最优的元素,值越小优先度越高,将其赋值给一个 “当前节点 ” current,它是不断更新的。
  2. 将 current 从open_list 中移除;同时添加进close_list中;
    在此需要简单说明一下“关闭集”close_list作用,所有的已考察过的节点都被添加进去,确保元素不会被重复考察。(如果往回走,就意味着无用的重复)。
  3. 遍历当前节点 current 的所有邻居 neighbor 节点,我们只考虑 可通过的 且 没有加入close_list 中(也即没有考察过)。接下来分为两种情况:
    1、该节点还没有被添加进 open_list 中:
    那么 计算该节点各函数的值 g(n),h(n),f(n) = g(n) + h(n);
    如果不明白三个函数什么意思的可以参考前一篇文章。
    2、如果该节点已经被添加进 open_list 中:
    那么我们就要根据情况判断是否需要对g(n),f(n)进行调整,以便使其值最小。
  4. 所有邻居节点 neighbor 考察完了,如果end 点在 open_list中,算法结束,路径找到。

    如果open_list已经空了,还是没有找到可取路径,那么就说明end点可能是一个孤立点。

说明:

算法我们已经大致明了,那么根据该例子进行一些必要的说明,在该例子中,我们假定有两种走法:垂直(边界)走 和 对角线走。二者的 耗费 cost 不同,假定垂直走->cost:10,对角线走->cost :14;此处取14只是为计算方便取10 *√2(根号2 ≈1.414)。

aStarT2

我们以绿格子为起点->current,开始遍历current 的所有neighbor ,正常情况下有8个neighbor,判断后进行函数计算g(n),h(n),f(n),并将其加入open_list中,将他们所有的父指针都指向current节点。父指针用于找到路径后的路径回溯。


之后是这样的:
aStarT3.2

函数计算:

  1. h(n) :h(n)是启发(heuristic)函数,可以根据自己的需要选用不同的启发函数,此处使用一个比较简单的函数:曼哈顿距离函数。
    则 h(n) = D * ( | thisPoint.x - end.x | + | thisPoint.y - end.y | )。
    以图示为例,start右侧方格 与 end 水平距离相差 3,竖直距离相差 0,而每一格Distance距离 为 10,所以 H = 30,通过这种方法,你甚至可以一开始就确定每一个格子的H值。
  2. g(n) = G = g(n-1) + g(n-1 -> n) :从start节点到current节点的G + 从current节点到当前搜索到的neighbor的cost。在该例中由于current = start,start->start :G = 0,因此只有start->该节点的cost 10;注意,这仍是不完备的,仅代表搜索到的节点未加入open_list中。
  3. f(n) = g(n) + h(n); 由此可见,估价函数f(n)是最后计算的。

经过上面的计算后,我们会发现current右侧的节点(图中为C)F值最小,那么通过循环下一个考察它:
aStarT43
通过考察C,我们发现:除了右侧三个墙壁是 不可通过的,左侧neighbor已经进入close_list,其余neighbor均已加入open_list,此时我们就要判断neighbor与C之间的关系(是否应该为父子关系)。
以C、D为例,现在D的父指针指向的是A节点start,D的G值是从A到D的距离数为14,而从C到D的G值通过 之前给出的公式 :G = g(n) = g(n-1) + g(n-1 -> n) ,计算结果就是从C看来,D的G值应该是20,这也就意味着,A-D 的距离 < A-C-D距离之和,因此,所以为了尽可能的减少距离,我们依然保持D的父节点是A,即

父节点是能让D节点到start节点 路径最短 的在D之前的第一个节点


上述很重要,可以说是最重要的一个调整,为了便于理解,我给出一个需要调整的反例:

aStarT44
还是上面这个图,但是不同的是在这个图中 节点C 和节点D之间不是普通的距离,他们之间存在一个桥B,桥B让C、D之间的距离缩短为2。
仍以C、D为例,现在D的父指针指向的是A节点start,D的G值是从A到D的距离数为14,而从C到D的G值通过 之前给出的公式 :G = g(n) = g(n-1) + g(n-1 -> n) ,计算结果就是G= 10 + 2,在C看来,D的G值应该是12,也就是说,从A到D的路程耗费 14 > 先从A到C,再由 C到D,耗费为12,因此为了减少耗费,我们就把D的G值调整为 12,F值调整为52,并把 它的父节点指针指向 C。
通俗的讲,为了从A到达D走更少的路,我决定走A-C-D这条路,而到D之前我经过的最后一个节点是C,因此D的父节点是C。


如果弄明白上述说明,那么看到这里就基本弄明白A* 算法了,接下来就是按部就班的走程序,回归原图:

aStarT51

假如说 ,考察完C之后,在open_list中有很多 优先度相同的节点,如D 、E,他们F值都是54,都属于最优先情况,我们假定先考察D。
(优先度相同时,至于具体想考察那个,完全可以自己控制,比如先考察排在前面的等)。

考察D,加两个新节点进入open_list,蓝色节点下方由于 “规定”不能越墙走对角线,所以不加入。
D考察完了,这时候虽然看起来D下方节点离红色节点更近,我们也要按照 F值 的高低来考察所有的有浅绿边框的节点。接下来是考察E。
然后是D左节点,然后是E左节点,然后是Start左侧节点,这三个的D值都是60,虽然他们看起来也不太可能对最终路径做贡献,但我们还是要逐个考察,直到外层的节点F值 都 >= 74,也即D下方节点重新是 优先度最高节点,我们才能继续D下方节点的考查。。。


完成结果

结果如下图所示:

aStarT6
所有以蓝色框圈住的节点都是已经考察过的节点,而绿色的就是open_list中待考察的节点。然而路径找到,算法停止。


路径回溯

通过父指针的使用,可以找从end到start的路径,图中以红点路径显示:

aStarT7


总结

本节主要介绍了A*算法的具体实现内容,如果可能,会在下一篇中给出之前几篇文章的代码(或许也会给出本人的粗陋代码),结合代码进行分析,同时指出之前一份代码的疏漏(可能),有兴趣的朋友不妨多去找找,A* 算法有很多经典代码的。最后,可能的话,也会给出一些算法的优化问题及方案。喜欢的朋友顶一下吧:)~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值