Introduction to the A* Algorithm

Reference

In games we often want to find paths from one location to another. We’re not only trying to find the shortest distance; we also want to take into account travel time. Move the blob (start point) and cross (end point) to see the shortest path.
在这里插入图片描述

To find this path we can use a graph search algorithm, which works when the map is represented as a graph.
A* is a popular choice for graph search. Breadth First Search is the simplest of the graph search algorithms, so let’s start there, and we’ll work our way up to A*.

Representing the map

The first thing to do when studying an algorithm is to understand the data. What is the input? What is the output?

Input: Graph search algorithms, including A*, take a “graph” as input.
A graph is a set of locations (“nodes”) and the connections (“edges”) between them. Here’s the graph I gave to A*:
在这里插入图片描述

  • A* doesn’t see anything else. It only sees the graph. It doesn’t know whether something is indoors or outdoors, or if it’s a room or a doorway, or how big an area is.
  • It only sees the graph! It doesn’t know the difference between this map and this other one.
    在这里插入图片描述
    Output: The path found by A* is made of graph nodes and edges.
    在这里插入图片描述
    The edges are abstract mathematical concepts. A* will tell you to move from one location to another but it won’t tell you how.
    Remember that it doesn’t know anything about rooms or doors; all it sees is the graph. You’ll have to decide whether a graph edge returned by A* means moving from tile to tile or walking in a straight line or opening a door or swimming or running along a curved path.

Tradeoffs: For any given game map, there are many different ways of making a pathfinding graph to give to A*. The above map makes most doorways into nodes;
在这里插入图片描述
what if we made doorways into edges? What if we used a pathfinding grid?
在这里插入图片描述
The pathfinding graph doesn’t have to be the same as what your game map uses. A grid game map can use a non-grid pathfinding graph, or vice versa.
A* runs fastest with the fewest graph nodes; grids are often easier to work with but result in lots of nodes.

This page covers the A* algorithm but not graph design; see my other page for more about graphs. For the explanations on the rest of the page, I’m going to use grids because it’s easier to visualize the concepts.

Algorithms

There are lots of algorithms that run on graphs. I’m going to cover these:

  • Breadth First Search explores equally in all directions. This is an incredibly useful algorithm, not only for regular path finding, but also for procedural map generation, flow field pathfinding, distance maps, and other types of map analysis.
    广度优先搜索在所有方向上均等地探索。 这是一种非常有用的算法,不仅适用于常规路径查找,还适用于程序地图生成、流场路径查找、距离图和其他类型的地图分析。
  • Dijkstra’s Algorithm (also called Uniform Cost Search) lets us prioritize which paths to explore. Instead of exploring all possible paths equally, it favors lower cost paths. We can assign lower costs to encourage moving on roads, higher costs to avoid forests, higher costs to discourage going near enemies, and more. When movement costs vary, we use this instead of Breadth First Search.
    Dijkstra 算法(也称为统一成本搜索)让我们优先考虑探索哪些路径。 它不是平等地探索所有可能的路径,而是倾向于成本较低的路径。 我们可以分配较低的成本来鼓励在道路上移动,较高的成本来避免森林,较高的成本来阻止接近敌人,等等。 当移动成本变化时,我们使用它而不是广度优先搜索。
  • A* is a modification of Dijkstra’s Algorithm that is optimized for a single destination. Dijkstra’s Algorithm can find paths to all locations; A* finds paths to one location, or the closest of several locations. It prioritizes paths that seem to be leading closer to a goal.
    A* 是 Dijkstra 算法的修改版,针对单个目的地进行了优化。 Dijkstra 算法可以找到所有位置的路径; A* 查找到一个位置或几个位置中最近的位置的路径。 它优先考虑似乎更接近目标的路径。

I’ll start with the simplest, Breadth First Search, and add one feature at a time to turn it into A*.

Breadth First Search

The key idea for all of these algorithms is that we keep track of an expanding ring called the frontier.
On a grid, this process is sometimes called “flood fill”, but the same technique also works for non-grids.
请添加图片描述
How do we implement this? Repeat these steps until the frontier is empty:

  1. Pick and remove a location from the frontier. 从边界选取并移除一个位置。
  2. Expand it by looking at its neighbors. Skip walls. Any unreached neighbors we add to both the frontier and the reached set. 通过查看它的邻居来扩展它。 跳过墙壁。 我们将任何未到达的邻居添加到边界和已到达集合中
    在这里插入图片描述
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)

This loop is the essence of the graph search algorithms on this page, including A*. But how do we find the shortest path?

The loop doesn’t actually construct the paths; it only tells us how to visit everything on the map.That’s because Breadth First Search can be used for a lot more than just finding paths; in this article I show how it’s used for tower defense, but it can also be used for distance maps, procedural map generation, and lots of other things.

Here though we want to use it for finding paths, so let’s modify the loop to keep track of where we came from for every location that’s been reached, and rename the reached set to a came_from table (the keys of the table are the reached set):
这里虽然我们想用它来寻找路径,所以让我们修改循环以跟踪我们到达的每个位置的来源,并将到达的集合重命名为 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

Now came_from for each location points to the place where we came from. These are like “breadcrumbs”. They’re enough to reconstruct the entire path. Move the cross to see how following the arrows gives you a reverse path back to the start position.
现在每个位置的 come_from 指向我们来自的地方。 这些就像“面包屑”。 它们足以重建整个路径。 移动十字,看看如何遵循箭头给你一个反向路径回到开始的位置。
在这里插入图片描述
The code to reconstruct paths is simple: follow the arrows backwards from the goal to the start. A path is a sequence of edges, but often it’s easier to store the nodes:重构路径的代码很简单:沿着箭头从目标到起点往回走。 路径是边的序列,但通常更容易存储节点:

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

That’s the simplest pathfinding algorithm. It works not only on grids as shown here but on any sort of graph structure. In a dungeon, graph locations could be rooms and graph edges the doorways between them. In a platformer, graph locations could be locations and graph edges the possible actions such as move left, move right, jump up, jump down. In general, think of the graph as states and actions that change state. I have more written about map representation here. In the rest of the article I’ll continue using examples with grids, and explore why you might use variants of breadth first search.
这是最简单的寻径算法。 它不仅适用于如图所示的网格,而且适用于任何类型的图形结构。 在地下城中,图形位置可以是房间,图形边缘是房间之间的门道。 在平台游戏中,图像位置可以是位置,图像边缘是可能的动作,如向左移动,向右移动,向上跳跃和向下跳跃。 一般来说,可以将图看作是改变状态的状态和动作。 我在这里写了更多关于地图表示的内容。 在本文的其余部分中,我将继续使用网格示例,并探讨为什么可能会使用广度优先搜索的变体。

Early exit

We’ve found paths from one location to all other locations. Often we don’t need all the paths; we only need a path from one location to one other location. We can stop expanding the frontier as soon as we’ve found our goal. Drag the around see how the frontier stops expanding as soon as it reaches the goal.
我们找到了从一个地点到所有其他地点的路径。 **我们通常不需要所有的路径; 我们只需要从一个位置到另一个位置的路径。 **一旦我们找到了目标,就可以停止扩张。

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

There are lots of cool things you can do with early exit conditions.

Movement costs

So far we’ve made step have the same “cost”. In some pathfinding scenarios there are different costs for different types of movement. For example in Civilization, moving through plains or desert might cost 1 move-point but moving through forest or hills might cost 5 move-points. In the map at the top of the page, walking through water cost 10 times as much as walking through grass. Another example is diagonal movement on a grid that costs more than axial movement. We’d like the pathfinder to take these costs into account. Let’s compare the number of steps from the start with the distance from the start:
到目前为止,我们已经采取了相同的“成本”。 在某些寻径场景中,不同类型的移动需要付出不同的代价。 例如,穿越平原或沙漠可能需要1个移动点,但穿越森林或山丘可能需要5个移动点。 在页面上方的地图上,走在水里的花费是走在草地上的10倍。 另一个例子是网格上的对角移动比轴向移动花费更多。 我们希望探索者能考虑到这些成本。 让我们比较一下从起点开始的步数和距离:
在这里插入图片描述
For this we want Dijkstra’s Algorithm (or Uniform Cost Search). How does it differ from Breadth First Search?

  • We need to track movement costs, so let’s add a new variable, cost_so_far, to keep track of the total movement cost from the start location.
  • We want to take the movement costs into account when deciding how to evaluate locations; let’s turn our queue into a priority queue.
  • Less obviously, we may end up visiting a location multiple times, with different costs, so we need to alter the logic a little bit.
  • Instead of adding a location to the frontier if the location has never been reached, we’ll add it if the new path to the location is better than the best previous path.

为此,我们需要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

Using a priority queue instead of a regular queue changes the way the frontier expands. Contour lines are one way to see this. Start the animation to see how the frontier expands more slowly through the forests, finding the shortest path around the central forest instead of through it:
使用优先级队列而不是常规队列会改变边界扩展的方式。 等高线是一种观察方法。 开始动画,看看边界如何在森林中更缓慢地扩展,找到围绕中心森林的最短路径,而不是穿过它:
在这里插入图片描述
在这里插入图片描述

Heuristic search

With Breadth First Search and Dijkstra’s Algorithm, the frontier expands in all directions. This is a reasonable choice if you’re trying to find a path to all locations or to many locations. However, a common case is to find a path to only one location. Let’s make the frontier expand towards the goal more than it expands in other directions. First, we’ll define a heuristic function that tells us how close we are to the goal:
通过广度优先搜索和Dijkstra算法,前沿向四面八方扩展。 这是一个合理的选择,如果你试图找到一个路径到所有或多个地点。 但是,常见的情况是只查找到一个位置的路径。 让边界向目标方向扩展,而不是向其他方向扩展。 首先,我们将定义一个启发式函数,告诉我们离目标有多近:

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

In Dijkstra’s Algorithm we used the actual distance from the start for the priority queue ordering. Here instead, in Greedy Best First Search, we’ll use the estimated distance to the goal for the priority queue ordering. The location closest to the goal will be explored first. The code uses the priority queue from Dijkstra’s Algorithm but without cost_so_far:
在 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

The A* algorithm

Dijkstra’s Algorithm works well to find the shortest path, but it wastes time exploring in directions that aren’t promising.
Greedy Best First Search explores in promising directions but it may not find the shortest path.
The A* algorithm uses both the actual distance from the start and the estimated distance to the goal.
The code is very similar to Dijkstra’s Algorithm:

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

Compare the algorithms: Dijkstra’s Algorithm calculates the distance from the start point. Greedy Best-First Search estimates the distance to the goal point. A* is using the sum of those two distances.

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值