感谢路径规划之a*这篇文章的步步引导,在此基础上,记下自己对a*算法的理解,以及启发式函数的理解。
1.广度优先搜索与Dijkstra:
设想一下算法在一个地图中寻找另外一点,最简单的方式就是从出发点开始,一圈圈向外膨胀。从起点开始,首先遍历起点周围邻近的点,然后再遍历已经遍历过的点邻近的点,逐步的向外扩散,直到找到终点。这便是广度有限算法bfs。这个过程像在火山的岩浆蔓延,逐渐占满空间直到达到目标点。然后从目标点回溯,得到最短路径。
在网格中移动,移动都周围的格子的代价都是走一步,是相同的。但是很多情况并非一样,比如在地图中,a城市到相邻的b、c城市距离并非一样。在移动代价不同的时候,就需要Dijkstra了。
在Dijkstra算法中,需要计算每一个节点距离起点的总移动代价。同时,还需要一个优先队列结构。对于所有待遍历的节点,放入优先队列中会按照代价进行排序。在算法运行的过程中,每次都从优先队列中选出代价最小的作为下一个遍历的节点。直到到达终点为止。
如果不考虑节点移动代价差异的广度优先搜索与Dijkstra算法,它们的结果将变得一致。
对于移动代价相同的情况,使用bfs这种方法显然是暴力的,做了很多无用功,它遍历了自身周围的全部点,而非优先选择某一些。这就引出了最佳优先搜索。
2.最佳优先搜索:
原理很简单,使用一个优先队列,以到达目标点的距离大小作为比较值。在添加了节点周边的点之后,优先选择距离目标点近的点。换句话说,原来bfs是一圈圈扩大,现在则是优先向着目标点走去。这可以大大加快路径的搜索速度。
但是缺点也很明显,如果起点与目标点之间存在障碍物,最佳优先搜索找到的就可能不是最优路径。打一个比方:小明在四合院中央要到四合院背后,眼前有两条路,从前门走,再绕到院子后面;直接向着院子后方走,但是要绕过一大堆的家具。在家具中经过的时间大于绕路走,但是最佳优先搜索只会指导向前走。
3.a*算法:
可以说,a算法是综合了以上的算法优点。a算法通过下面这个函数来计算每个节点的优先级:
f(n) = g(n) + h(n)
这里的g(n)
可以看做bfs的移动代价,h(n)
则是最佳优先搜索的代价。
- f(n)是节点n的综合优先级。当我们选择下一个要遍历的节点时,我们总会选取综合优先级最高(值最小)的节点。
- g(n) 是节点n距离起点的代价。
- h(n)是节点n距离终点的预计代价,这也就是a*算法的启发函数。关于启发函数我们在下面详细讲解。
a*
算法在运算过程中,每次从优先队列中选取f(n)值最小(优先级最高)的节点作为下一个待遍历的节点。
另外,a*
算法使用两个集合来表示待遍历的节点,与已经遍历过的节点,这通常称之为open_set和close_set
* 初始化open_set和close_set;
* 将起点加入open_set中,并设置优先级为0(优先级最高);
* 如果open_set不为空,则从open_set中选取优先级最高的节点n:
* 如果节点n为终点,则:
* 从终点开始逐步追踪parent节点,一直达到起点;
* 返回找到的结果路径,算法结束;
* 如果节点n不是终点,则:
* 将节点n从open_set中删除,并加入close_set中;
* 遍历节点n所有的邻近节点:
* 如果邻近节点m在close_set中,则:
* 跳过,选取下一个邻近节点
* 如果邻近节点m也不在open_set中,则:
* 设置节点m的parent为节点n
* 计算节点m的优先级
* 将节点m加入open_set中
启发函数
上面已经提到,启发函数会影响A*算法的行为。
- 在极端情况下,当启发函数h(n)始终为0,则将由g(n)决定节点的优先级,此时算法就退化成了Dijkstra算法。
- 如果h(n)始终小于等于节点n到终点的代价,则
a*
算法保证一定能够找到最短路径。但是当h(n)的值越小,算法将遍历越多的节点,也就导致算法越慢。 - 如果h(n)完全等于节点n到终点的代价,则
a*
算法将找到最佳路径,并且速度很快。可惜的是,并非所有场景下都能做到这一点。因为在没有达到终点之前,我们很难确切算出距离终点还有多远。 - 如果h(n)的值比节点n到终点的代价要大,则
a*
算法不能保证找到最短路径,不过此时会很快。
在另外一个极端情况下,如果h()n相较于g(n)大很多,则此时只有h(n)产生效果,这也就变成了最佳优先搜索。
由上面这些信息我们可以知道,通过调节启发函数我们可以控制算法的速度和精确度。因为在一些情况,我们可能未必需要最短路径,而是希望能够尽快找到一个路径即可。这也是a*
算法比较灵活的地方。
对于网格形式的图,有以下这些启发函数可以使用:
- 如果图形中只允许朝上下左右四个方向移动,则可以使用曼哈顿距离(Manhattan distance)。
- 如果图形中允许朝八个方向移动,则可以使用对角距离。
- 如果图形中允许朝任何方向移动,则可以使用欧几里得距离(Euclidean distance)。
a*总结
总而言之,就是如何在set中选择下一个待遍历的节点。在bfs
中按谁先进入set选谁,最佳优先搜索
按谁离得目标近选谁,a*
则是用它们两的综合选择。另外一方面,启发函数十分重要,决定了速度与精度。
4.发式函数的理解
启发函数是一种思想,它给算法选择增加了一个度量,在算法增加信息,在不知道如何选择下一个呆遍历的节点时候,指导算法选择。
换言之,bfs并没有充分利用好全部的已知信息,没有将目标点位置的信息利用。甚至a*
也没有,是不是甚至可以创造一个启发函数H_1()
,它描述了目标点距离障碍物的距离或者方向,当节点到达障碍物附近时候,指导下一个节点的选择。只不过这样的启发函数带来的算法复杂度在效率上是否划算。
在优达学城的自动驾驶入门课中,学习到了,我们还可以利用多个启发函数,比如在H_1()``H_2()
都未大于真实代价的情况下,我们可以:
H() = max( H_1(), H_2() )
这样是不是更加机智