在A*寻路算法中使用二元堆(上)

来源:互联网 发布:淘宝网天猫羽绒服女款 编辑:程序博客网 时间:2024/04/29 22:23

                                   A*寻路算法中使用二元堆(上)

      By Patrick Lester ( Updated April 11,2003)

         翻译:[asylum]amdk_7

这篇文章是配合我的另一篇文章, A* Pathfinding for Beginners.”,在阅读这篇文章之前您应该十分理解A*寻路算法或者仔细的度过我那篇文章.

    A*寻路算法里,从开放列表寻找F值最小结点并进行扩展是最耗时的部分之一.这个耗时取决于你地图的大小,你可能有不同数量级的开放列表结点需要搜索.无庸置疑的,重复不断的搜索这样一个列表会使程序变的很慢.无论如何,这在很大程度上取决于你存储列表所使用的方式.

   

有序和无序开放列表:一种简单的优化

    我们在“A* Pathfinding for Beginners.”所使用的简单方法是直接存储结点,这样每次你需要选取最小F值结点并进行扩展时都要搜索整个开放列表.这种方法有着最快的结点插入速度,但是也有着最慢的扩展结点的速度.

    我们可以做一些简单的改进就是使用有序列表.这需要一些前期的工作,因为在你每次向列表中添加结点的时候,你需要找到适当的插入位置.扩展结点只用简单的取出列表的第一个结点就可以了.

    有很多种方法可以使你的列表是有序的(选择排序,冒泡排序,快速排序等等)并且你可以在网络上搜索到这些算法.不过在这里用不到这些.一个简单的方式是每次在你添加新结点时,从前到后的比对表中的结点,并插入此新结点到第一个比它的F值大的结点之前.建议使用LISTS数据结构.

    下面有一个技巧,你可以保存一个表中结点F的平均值,并用这个平均值来决定你是正序搜索还是逆序搜索.一般来说,如果新结点的F值小于平均值就采用正序搜索,反之亦然,这个技巧可以将你的搜索时间减半.

    另一种更复杂但快速的方法,思想近似与快速排序,基本思想是这样的,开始时你需要比较新结点和位于列表中间结点F值的大小,若新结点较小,继续比较新结点和表中1/4位置的结点,这样比较下去,直到找到正确的位置为止.这只是一种简短的描述,你可以在网络上搜索一些有关快速排序的文章并了解一下快速排序工作的原理。这应该是比任何排序技术都快的算法了。

 

二元堆

    二元堆在描述上与快速排序及其的相似,并且他们经常被人们用来加快的A*函数的速度上。根据我的经验,使用二元堆能够提高寻路算法的速度23倍,在更大的地图中提高的会更多。不论怎样,选择使用二元堆算法能够让你不会在速度问题上头痛。

    下面的内容是对在A*算法中使用二元堆的具体说明,在本文尾部更进一步的文章可以供你阅读。

    如果你仍然感兴趣,那我们就继续把。。。。

    在一个有序的链表中,每一个结点都在其适当的位置,正序或者逆须的。这确实有些帮助,但事实上不是我们最想要的。我们并不关心在表中127位置的结点一定要小于128的结点,我们只是想要最小F消耗的结点。保持这个表是有序的只不过是一个非必须的工作,当然,我们也需要下一个最小结点。

    基本上,我们只需要一些被称之为堆的东西,或者特殊的叫做二元堆。一个二元堆只不过是简单的约束最大或者最小的结点(根据你的需要)在堆顶端。因为我们需要寻找F消耗最小的结点,我们要将其放在堆的顶端。这个结点有两个子结点,结点的值等于或稍稍大于顶端结点的F值,根据这种规则建立起此二元堆,下面有一个简单的二元堆:

       请注意:最小结点10位于堆的顶端,第二小的结点是其一个子结点。在这个特殊的二元堆结构中,第三小的结点是24,深度是2,并且小于跟结点的左子结点30。简单的放置,只要保证子结点的值大于或者等于其父结点的值就足够了,与结构中别的结点的大小是无所谓的。根据这种规则建立的堆结构就是有效的二元堆了。

       你现在可能在想怎样实际应用这种结构,呵呵,最简单的一种方法是使用一位数组来存储。

       在这个数组中,堆顶的元素位于数组最前面的位置(比如在1 的位置),他的两个子结点的位置就是23,第三层结点的位置就是47

       一般来讲,任何结点的两个子结点的位置可以通过此结点在数组中的位置乘以2得出。例如第三个结点的两个子结点在数组中位置是 3*2 = 63*2+1 = 7

       你只需要保证在树最底层之上是完整的就可以了,文章下面会具体说明做法。

      

 

向树中添加结点

       我们必须在使用二元堆处理寻路算法前考虑很多事情,不过我们现在还是来看一看一些基本的二元堆操作。如果你对二元堆的操作很了解,我建议你跳过这一部分。我在稍后会给出一些基本的操作规则,不过最好了解一下理论。

       添加一个结点到一个二元堆的末尾,我们需要和其父结点进行比较,父结点的位置是子结点的位置除以2,如果子结点小于父结点,就交换其二的位置,并且将新结点与其新的父结点进行比较,一直重复这种操作,直到新结点找到合适的位置或者位于二元堆的头部。

例如:

我们添加一个F值为17的结点到我们的堆中。在数组中已经有7个结点,所以将其先防治在第8的位置,如下:

10 30 20 34 38 30 24 17

接下来我们将新结点和父结点进行比较,父结点的位置是8/2 4。在4位置结点的F值是3417小于34,交换二者,如下:

10 30 20 17 38 30 24 34

继续和父结点进行比较,父结点位置是4/2 2。在2位置结点的F值是3017要小于30,交换,如下:

10 17 20 30 38 30 24 34

继续比较,现在我们的位置是2,比较2117大于34,停止操作。

 

在二元堆中删除结点

删除结点有着和上面相似的过程,不过步骤是相反的。(这里的例子是删除位于位置一的结点,其他雷同)首先,我们移去位置1的结点,现在这个位置是空的了,然后我们将最后一个结点移动到位置1

34 17 20 30 38 30 24

接下来我们比较这个结点和他的两个子结点,两个子结点的位置分别位于(当前位置*2)和(当前位置*2 + 1)。如果它比这两个结点的值都小,那么就可以结束比较了,他处于他应该在的位置了。如果不是这种情况,将他与两个子结点中比较小的那一个结点进行交换。在这个例子中,两个子结点的位置分别是23,进行比较后得出34要大于他们两个的值,所以我们和比较小的子结点的值进行交换。

17 34 20 30 38 30 24

接下来我们继续比较这个结点和其新的两个子结点的值,两个子结点的位置分别是45。得出他要大于子结点的值, 所以进行交换:

17 30 20 34 38 30 24

最后我们比较他和新子结点的值。不过已经到了树的底端,我们完成了这个任务。

 

为什么二元堆这么快?

    现在你知道了插入与删除两个基本操作,你可能正在思考为什么它这么快。想象一下,你现在有一个100*100的地图(10000个结点)。如果我们使用简单的插入排序,从表头开始,一个个进行比较直到找到一个合适的位置插入新的结点,需要平均进行500次的比较操作。

    使用二元堆,进行插入操作平均只需要13次比较,删除操作也平均9次就组投了。在A*算法中,每一次操作需要取出1个结点,插入05个新结点(2D)。他们总共加起来只需要插入排序方法1%的时间。这种差距根据你地图大小的不同有几何增长。

    不过,使用二元堆并不意味着你的算法就会有100倍的速度优势。这里有一些棘手的问题,我会在下面描述。况且,A*算法不单单是简单的排序。在我的经验中,一般使用二元堆可以另你的A*寻路算法提高23倍的速度,而且随着路线的增长优势会更加明显。

 

(未完)

 

 

 

 
原创粉丝点击