封面《花咲ワークスプリング!》
因为某些原因需要接触 A * 算法,所以记录一下
A* 搜索算法
A * 搜索算法是一种带有启发式算法的路径规划算法,由 Stanford 研究院的 Peter Hart, Nils Nilsson 以及 Bertram Raphael 于 1968 年提出,现在仍在使用。结合了最佳优先算法 (Best first Search) 和 Dijkstra 算法的优点,被广泛的应用于游戏等领域。
最佳优先算法 (Best first Search)
通常深度优先算法和广度优先算法都是将任何相邻节点视为下一个需要遍历的节点直接遍历,都是盲目的探索路径而没有考虑遍历成本,此处的成本是指人为定义的成本函数,后续会提到。而最佳优先算法的思想是采用一种评价函数来判断哪个节点更优先进行探索。
其伪代码如下所示
1 | Best-First-Search(Graph g, Node start) |
下面将用代码对其进行展示,先构造一个迷宫,其中绿色表示其迷宫的起点,红色表示终点,黑色表示障碍物,白色表示可通行的路径。
1 | %matplotlib inline |
最佳优先算法其执行结果如下,浅蓝色和浅绿色为其进行搜索过节点,蓝色为算法求得的最短路线。该算法的优点是可以快速找到一条路径,但是这条路径不一定是最优路径,因为它只是根据评价函数来判断哪个节点更优先进行探索,而没有考虑到整体的路径成本。
1 |
|
best_first_search耗时:0.0009s
Dijkstra 算法
Dijkstra 算法是一种贪心算法,用于计算图中的最短路径。它的基本思想是从起点开始,逐步扩大搜索范围,直到找到终点为止。Dijkstra 算法是一种动态规划算法。其先找到相邻节点的最短路径,再基于该路径继续寻找其他节点的最短路径,其伪代码如下所示。
1 | function Dijkstra(Graph, source): |
下图为 Dijkstra 算法的执行结果,浅蓝色和浅绿色为其进行搜索过节点,蓝色为算法求得的最短路线。Dijkstra 算法的优点是可以找到最短路径,但是可以看到其遍历的节点相较于最佳优先算法要多很多,因为它是根据起点到每个节点的距离来进行搜索的,而不是根据评价函数来判断哪个节点更优先进行探索。
1 | import math |
dijkstra耗时:0.0026s
A * 算法
从前面的最佳优先算法和 Dijkstra 算法可以看出,最佳优先算法需要遍历的节点数较少,但是无法保证可以找到最优路径,而 Dijkstra 算法可以找到最优路径,但是需要遍历的节点数较多。A * 算法是一种结合了最佳优先算法和 Dijkstra 算法的优点的路径规划算法。其基本思想是综合考虑起点到当前节点的距离和当前节点到终点的距离,通过评价函数来判断哪个节点更优先进行探索。其伪代码如下所示。
1 | // A* Search Algorithm |
A* 算法采用了一种启发式评价函数来判断哪个节点更优先进行探索,其评价函数是 f (n) = g (n) + h (n),其中 g (n) 是从起点到当前节点的距离,可以视为历史代价,h (n) 是启发函数算得的从当前节点到终点的距离,可以视为未来预期代价。当 g (n) 为 0 时 A* 算法就退化为了最佳优先算法,当 h (n)=0 时,A* 算法就退化为了 Dijkstra 算法。A* 算法的优点是可以找到最优路径,同时遍历的节点数也相对较少。下图为 A* 算法的执行结果,浅蓝色和浅绿色为其进行搜索过节点,蓝色为算法求得的最短路线。可以看到 A* 的算法效果与 dijkstra 算法相似,但是遍历的节点数要少很多。
$ f(n) = g(n) + h(n) $
1 |
|
astar耗时:0.0035s
启发函数 (heuristic function)
启发函数告诉 A* 算法去预估到目标点的代价,当启发函数 h (n) 为 0 时,A* 算法退化为了 Dijkstra 算法,当 A* 算法值较小时,A* 算法能够保证找到最短路径,h (n) 越小 A* 算法要遍历的节点越多,速度越慢。当 h (n) 相较于 g (n) 特别大的时候,算法退化为最佳优先算法。当 h (n) 稍大于 g (n) 时,A* 算法无法保证能够找到最短路径,但是运行速度很快。当 h (n) 与 g (n) 量级相接近时,A* 算法呢能够找到最短路径,同时遍历的节点数也相对较少,是最理想的情况。
除了 h (n) 规模意外,启发函数自身也起很重要的作用,当启发函数恰好为最佳路径的距离时,A* 算法会遍历很少的节点。当 h (n) 与 g (n) 完全匹配时,f (n) 的值不会随着路径变化,此时 A* 算法不会偏离最短路径,但是这种情况很理想的情况。下面将介绍一些启发算法
曼哈顿距离 (Manhattan Distance)
方形网格标准启发函数时曼哈顿距离,其计算方式是两点之间的横纵坐标的差的绝对值之和。其公式如下所示,比较适合作为在方形网格上四个方向上的移动的启发函数。
1 | function heuristic(node) = D * (abs(node.x-goal.x) + abs(node.y-goal.y)) |
对角线距离 (Diagonal Distance)
如果允许在方形网格上进行对角线移动,那么对角线距离是一个更好的启发函数。其计算方式是两点之间的横纵坐标的最大差的绝对值。其公式如下所示。
1 | function heuristic(node) = |
当 D=1、D2=1
时,对角线距离就是切比雪夫距离。当 D=1、D2=sqrt(2)
时,对角线距离就是 Octile 距离。
欧拉距离 (Euclidean Distance)
如果能够在任意方向上移动,那么欧拉距离是一个更好的启发函数。其计算方式是两点之间的直线距离。其公式如下所示。
1 | function heuristic(node) = |
breaking ties
在 A* 算法中,如果有多个节点的 f (n) 值相等。例如,在地形没有变化的平坦区域上,会有多条具有相同长度的最短路径,A* 算法就会探索所有具有相同 f 值的路径。这种情况称为 ties。
解决这个问题的快速方法是调整 g 或者 h 值,使得 f 值不相等,A* 算法按照值进行排序,这意味着 A* 算法会优先探索具有最小 f 值的节点,不会同时探索那些 f 值相同的节点。一种打破方法是将 h 向上调整(如果向下调整会使得 A* 算法更倾向于寻找靠近起点的节点)。其公式如下所示:
1 | heuristic *= (1.0 + p) |
当有障碍物时依然需要探索寻找绕开障碍物的路径,但是越过障碍物后 A* 需要探索的节点数会减少。
此外还可以在 f 值相同的情况下,对 f 进行比较,或者在启发函数中引入确定的随机数
A* 变种
一种变种是加权 A* 算法,通过对启发函数进行甲醛可以让 A* 算法运行更快,其公式如下所示:
1 | f(n) = g(n) + w * h(n) |
前面实现的 A* 算法就是加权 A* 算法
另一变种是动态加权 A* 算法,在不同的位置会有不同的加权启发,其公式如下所示:
1 | f(n) = g(n) + w(n) * h(n) |
此外还有带宽搜索 (bandwidth)、双向搜索 (Bidirectional Search) 等变种,具体的可以看这篇斯坦福的文章
demo
这里推荐几个动态的 A * 等算法 demo,特别是斯坦福的动画做的很好,让人很容易懂