A* 算法基础

Dijkstra算法

  • Dijkstra算法用来寻找图形中节点之间的最短路径。
  • 在Dijkstra算法中,需要计算每一个节点距离起点的总移动代价。同时,还需要一个优先队列结构。对于所有待遍历的节点,放入优先队列中会按照代价进行排序。
  • 在算法运行的过程中,每次都从优先队列中选出代价最小的作为下一个遍历的节点。直到到达终点为止。
  • 下面对比了不考虑节点移动代价差异的广度优先搜索与考虑移动代价的Dijkstra算法的运算结果:

当图形为网格图,并且每个节点之间的移动代价是相等的,那么Dijkstra算法将和广度优先算法变得一样。

最佳优先搜索

  • 在一些情况下,如果我们可以预先计算出每个节点到终点的距离,则我们可以利用这个信息更快的到达终点。
  • 其原理也很简单。与Dijkstra算法类似,我们也使用一个优先队列,但此时以每个节点到达终点的距离作为优先级,每次始终选取到终点移动代价最小(离终点最近)的节点作为下一个遍历的节点。这种算法称之为最佳优先(Best First)算法。
  • 这样做可以大大加快路径的搜索速度,如下图所示:

最佳优先算法

  • 但这种算法会不会有什么缺点呢?答案是肯定的。
  • 因为,如果起点和终点之间存在障碍物,则最佳优先算法找到的很可能不是最短路径,下图描述了这种情况。

最佳优先算法的缺点

A* 算法

  • A* 算法实际上是综合上面这些算法的特点于一身的。
  • A* 算法通过下面这个函数来计算每个节点的优先级。
  • 其中:

    • f(n)是节点n的综合优先级。当我们选择下一个要遍历的节点时,我们总会选取综合优先级最高(值最小)的节点。
    • g(n) 是节点n距离起点的代价。
    • h(n)是节点n距离终点的预计代价,这也就是A*算法的启发函数。关于启发函数我们在下面详细讲解。
  • A*算法在运算过程中,每次从优先队列中选取f(n)值最小(优先级最高)的节点作为下一个待遍历的节点。

  • 另外,A*算法使用两个集合来表示待遍历的节点,与已经遍历过的节点,我称之为Open_ListClose_List
  • 完整的A*算法描述如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
* 初始化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)。

关于距离

曼哈顿距离
  • 如果图形中只允许朝上下左右四个方向移动,则启发函数可以使用曼哈顿距离,它的计算方法如下图所示:

曼哈顿距离

  • 计算曼哈顿距离的函数如下。
1
2
3
4
5
6
7
8
9
10
/// <summary>
/// 曼哈顿距离
/// </summary>
/// <param name="v1">坐标1</param>
/// <param name="v2">坐标2</param>
/// <returns>两个坐标之间的曼哈顿距离</returns>
float ManhattanDistance(Vector2 v1, Vector2 v2)
{
return Mathf.Abs(v1.x - v2.x) + Mathf.Abs(v1.y - v2.y);
}
对角距离 与 欧拉距离
  • 如果图形中允许斜着朝邻近的节点任意方向 移动,则启发函数可以使用欧拉距离。
  • 其函数表示如下(实际使用时通常不开根号,可以提高精度):
1
2
3
4
5
6
7
8
9
10
11
/// <summary>
/// 欧拉距离的平方
/// </summary>
/// <param name="v1">坐标1</param>
/// <param name="v2">坐标2</param>
/// <returns>两个坐标之间的欧拉距离的平方</returns>
float EulerDistance(Vector2 v1, Vector2 v2)
{
// 统一不开根号,这样可以提高精度
return Mathf.Pow(v1.x - v2.x, 2) + Mathf.Pow(v1.y - v2.y, 2);
}

小结

  • 调用AStar算法时需要自行根据项目需要调节启发函数(即下方代码中的CalculateH函数)和判断邻近节点合法性函数(即下方代码中的FindAdjacentNodesAndAddThemToTheOpenList函数)
  • A* 参考代码