A*算法原理及演变历程(一)

frontier = Queue()
frontier.put(start )
reached = set()
reached.add(start)

while not frontier.empty():
   current = frontier.get()
   for next in graph.neighbors(current):
      if next not in reached:
         frontier.put(next)
         reached.add(next)

 这个循环是本页图搜索算法的精髓,包括A*。但是我们如何找到最短路径呢?循环实际上并不构建路径;它只告诉我们如何访问地图上的所有内容。那是因为广度优先搜索不仅可以用于查找路径,还可以用于更多用途。在本文中,我展示了它如何用于塔防,但它也可以用于距离地图、程序地图生成和许多其他事情。在这里,虽然我们想用它来寻找路径,所以让我们修改循环以跟踪我们从哪里到达的每个位置,并将reached集合重命名为一个came_from表(表的键是到达的集合):

frontier = Queue()
frontier.put(start )
came_from = dict()
came_from[start] = None

while not frontier.empty():
   current = frontier.get()
   for next in graph.neighbors(current):
      if next not in came_from:
         frontier.put(next)
         came_from[next] = current

 现在came_from每个位置都指向我们来自的地方。这些就像“面包屑”。它们足以重建整个路径。移动十字以查看跟随箭头如何为您提供返回起始位置的反向路径。

重建路径的代码很简单:沿着箭头从目标向后移动到起点。路径是一系列边,但通常更容易存储节点:

current = goal 
path = []
while current != start: 
   path.append(current)
   current = came_from[current]
path.append(start) # optional
path.reverse() # optional

这是最简单的寻路算法。它不仅适用于此处所示的网格,而且适用于任何类型的图形结构。在地牢中,图形位置可以是房间,图形边缘是它们之间的门口。在平台游戏中,图形位置可以是位置和图形边缘,可能的动作例如左移、右移、上跳、下跳。通常,将图表视为改变状态的状态和动作。我在这里写了更多关于地图表示的文章。在本文的其余部分中,我将继续使用带有网格的示例,并探讨为什么您可能会使用广度优先搜索的变体。

提前退出

我们找到了从一个位置到所有其他位置的路径。通常我们不需要所有的路径;我们只需要从一个位置到另一个位置的路径。一旦我们找到了目标,我们就可以停止扩展边界。拖动四周,看看边界到达目标后如何停止扩展。

frontier = Queue()
frontier.put(start )
came_from = dict()
came_from[start] = None

while not frontier.empty():
   current = frontier.get()

   if current == goal: 
      break           

   for next in graph.neighbors(current):
      if next not in came_from:
         frontier.put(next)
         came_from[next] = current

移动成本

到目前为止,我们已经使 step 具有相同的“成本”。在某些寻路场景中,不同类型的移动会产生不同的成本。例如在《文明》中,穿越平原或沙漠可能需要 1 个移动点,但穿越森林或丘陵可能需要 5 个移动点。在页面顶部的地图中,在水中行走的成本是在草丛中行走的 10 倍。另一个例子是网格上的对角线移动,其成本高于轴向移动。我们希望探路者将这些成本考虑在内。让我们比较从开始的步数和距离开始的距离:

为此,我们需要Dijkstra 算法(或统一成本搜索)。它与广度优先搜索有何不同?我们需要跟踪移动成本,所以让我们添加一个新变量cost_so_far来跟踪从起始位置开始的总移动成本。在决定如何评估位置时,我们希望将移动成本考虑在内;让我们把队列变成优先队列。不太明显的是,我们最终可能会以不同的成本多次访问一个位置,因此我们需要稍微改变一下逻辑。如果该位置从未到达过,我们不会将位置添加到边界,而是如果到该位置的新路径优于之前的最佳路径,我们将添加它。

frontier = PriorityQueue()
frontier.put(start, 0)
came_from = dict()
cost_so_far = dict()
came_from[start] = None
cost_so_far[start] = 0

while not frontier.empty():
   current = frontier.get()

   if current == goal:
      break
   
   for next in graph.neighbors(current):
      new_cost = cost_so_far[current] + graph.cost(current, next)
      if next not in cost_so_far or new_cost < cost_so_far[next]:
         cost_so_far[next] = new_cost
         priority = new_cost
         frontier.put(next, priority)
         came_from[next] = current

 实现说明:我们希望这个优先级队列首先返回最低值。

启发式搜索

使用广度优先搜索和 Dijkstra 算法,边界向各个方向扩展。如果您试图找到通向所有位置或多个位置的路径,这是一个合理的选择。然而,一种常见的情况是只找到一个位置的路径。让我们让边界向目标扩展多于向其他方向扩展。首先,我们将定义一个启发式函数,告诉我们离目标有多近:

def heuristic(a, b):
   # Manhattan distance on a square grid
   return abs(a.x - b.x) + abs(a.y - b.y)

在 Dijkstra 算法中,我们使用从开始的实际距离来进行优先级队列排序。相反,在Greedy Best First Search中,我们将使用到目标的估计距离来进行优先队列排序。最接近目标的位置将首先被探索。该代码使用 Dijkstra 算法中的优先级队列,但没有cost_so_far:

frontier = PriorityQueue()
frontier.put(start, 0)
came_from = dict()
came_from[start] = None

while not frontier.empty():
   current = frontier.get()

   if current == goal:
      break
   
   for next in graph.neighbors(current):
      if next not in came_from:
         priority = heuristic(goal, next)
         frontier.put(next, priority)
         came_from[next] = current

这些路径并不是最短的。所以这个算法在没有很多障碍物的情况下运行得更快,但是路径不是很好。我们能解决这个问题吗?是的!

A* 算法

Dijkstra 算法可以很好地找到最短路径,但它会浪费时间去探索没有希望的方向。Greedy Best First Search 探索有希望的方向,但可能找不到最短路径。A* 算法使用从起点的实际距离和到目标的估计距离。

该代码与 Dijkstra 的算法非常相似:

frontier = PriorityQueue()
frontier.put(start, 0)
came_from = dict()
cost_so_far = dict()
came_from[start] = None
cost_so_far[start] = 0

while not frontier.empty():
   current = frontier.get()

   if current == goal:
      break
   
   for next in graph.neighbors(current):
      new_cost = cost_so_far[current] + graph.cost(current, next)
      if next not in cost_so_far or new_cost < cost_so_far[next]:
         cost_so_far[next] = new_cost
         priority = new_cost + heuristic(goal, next)
         frontier.put(next, priority)
         came_from[next] = current

 比较算法:Dijkstra 算法计算距起点的距离。Greedy Best-First Search 估计到目标点的距离。A* 使用这两个距离的总和。

尝试在墙上的各个地方开一个洞。您会发现,当贪婪的最佳优先搜索找到正确答案时,A* 也会找到它,并探索同一区域。当贪婪的最佳优先搜索找到错误的答案(更长的路径)时,A* 会找到正确的答案,就像 Dijkstra 算法一样,但仍然比 Dijkstra 算法探索得少。

A* 是两全其美的。只要启发式算法没有高估距离,A* 就会找到一条最佳路径,就像 Dijkstra 算法所做的那样。A* 使用启发式方法对节点进行重新排序,以便更快地遇到目标节点。

而且……就是这样!这就是 A* 算法

文章搬运于https://www.redblobgames.com/
Copyright © 2022 Red Blob Games
作者 Amit Patel

posted on 2022-03-16 10:52  -G  阅读(583)  评论(0)    收藏  举报

导航