首先解释一下状态空间搜索。状态空间搜索法就是将问题求解过程表现为从 初始状态到目标状态寻找这个路径的过程。
通俗点说,就是在解一个问题时,找到一条解题的过程可以从 求解的开始到问题的结果。由于求解问题的过程中分枝有很多,主要是求解过程中求 解条件的不确定性,不完备性造成的,使得求解的路径很多这就构成了一个图,我们说这个图就是状态空 间。问题的求解实际上就是在这个图中找到一条路径可以从开始到结果。这个寻找的过程就是状态空间搜索。
常用的状态空间搜索有深度优先和广度优先。广度优先是从初始状态一层一层向下找,直到找到目标 为止。深度优先是按照一定的顺序前查找完一个分支,再查找另一个分支,以至找到目标为止。
显然,只要目标点存在,广度优先(BFS)或深度优先(DFS)搜索算法都一定能够搜索到目标点。但是,当结点数很多时,BFS和DFS的空间复杂度和时间复杂度都变得不可接受。而它们的效率不高的一个重要原因在于没有利用任何已有的信息。而A*算法由于利用了启发信息,因而获得了较高的搜索效率。
我们先下个定义,如果一个估价函数可以找出最短的路径,我们称之为可采纳性。A算法是一个可采纳的最好优先算法。A算法的估价函数可表示为:
f'(n) = g'(n) + h'(n)
这里,f’(n)是估价函数,g’(n)是起点到节点n的最短路径值,h’(n)是n到目标的最短路经的启发值。由于这个f’(n)其实是无法预先知道的,所以我们用前面的估价函数f(n)做近似。g(n)代替g’(n),但 g(n)>=g’(n)才可(大多数情况下都是满足的,可以不用考虑),h(n)代替h’(n),但h(n)<=h’(n)才可(这一点特别的重要)。可以证明应用这样的估价函数是可以找到最短的,也就是可采纳的。我们说应用这种估价函数的最好优先算法就是A算法。
因而实际在算法中采用的估价函数为:f(n)=g(n)+h(n),其中g(n)为起始点到当前点的实际代价,而h(n)为当前点到目标点的启发信息,目前,在二维平面内常用的A算法的启发函数h(n)有曼哈顿距离、对角线距离、欧几里德距离。也有学者采用所谓“折距”作为启发函数。但是,采用对角线距离的话会太保守,搜索效率不高,而采用曼哈顿距离或者“折距”,虽然可以提高效率,但是都不能保证满足可接纳性条件,因而不宜采用。目前用于二维平面内的A*算法普遍采用欧几里德距离。
举一个例子,其实广度优先算法(BFS)就是A算法的特例。其中g(n)是节点所在的层数,h(n)=0,这种h(n)肯定小于h’(n),所以由前述可知广度优先算法是可采纳的。实际也是。当然它是一种最差的A算法。
通过上面的介绍,我们可以确定A*算法的流程如下(注:出自wikipedia):
|
|
这里必须吐槽一下度娘的A算法流程是不严谨的(如果想要理解A算法的同学,建议还是看英文版维基上的。当然,最好就是借一本人工智能方面的书认真看一下,可以理解得更透彻。
当然,上面只是最原始的A*算法。如果要获得较高的效率,还需要对其进行进一步的改进,其中目前大多数学者优化的重点都放在启发信息的改进和从Open表中更快地获取代价最小的结点。
其中,关于启发信息的讨论在前面已经提到过,而且由于它一般要与具体的学科问题相结合才有意义,此处不再赘述。
下面主要讲解如何能更快地从Open表中获取代价最小的节点。显然有两个途径:第一,根据具体的问题,预先将不符合某些约束(比如在航迹规划时,将不符合动力学约束的节点去除。或者说,我们只在符合动力学约束的空间内选择邻节点放入表中)的子节点剪除掉。
第二种方法就是在邻节点数无法改变的情况下,选择一种好的排序算法就至关重要了。可以选择快速排序等,目前较常用的一种高效排序算法是维持一个称为“最小堆”的数据结构,它是一种二叉树结构,其特征是对于任一结点,它的左孩子(如果有的话)和右孩子必定大于它。显然,这种排序算法的时间复杂度为O(log2N)。排好序后,每次只要将根结点移除即可。如下图所示。
A算法的理论介绍基本就这些。下面是自己写的一个利用A算法寻找二维平面内任意两点间的最短路径,希望能够抛砖引玉。
由于在节点的扩展过程中需要实现较多的操作,而这些操作与节点密切相关。因而,为了实现更好的封装,此处没有使用C++中的STL,而是自己实现了链表结构。
首先是LisNode的定义:
|
|
下面是ListNode的实现部分,即listNode.cpp文件:
|
|
下面是链表的定义:
|
|
下面是其实现:
|
|
最后是main函数,它与上面介绍的A*算法几乎一样,只是自己作了一点小的改进:
|
|
以上。