深度讲解 寻址算法 A DIJKSTRA 贪婪算法 三种算法的对比 在部分复杂项目 无路径 避障 等复杂场合 可能会用到 有兴趣的可以自学下*
在游戏中,我们经常想要找到从一个位置到另一个位置的路径。我们不仅试图找到最短的距离;我们还需要考虑旅行时间。移动 blob(起点)和交叉(终点)以查看最短路径。
要找到此路径,我们可以使用图形搜索算法,该算法在将地图表示为图形时起作用。A*是图形搜索的流行选择。广度优先搜索是最简单的图形搜索算法,所以让我们从那里开始,我们将一路上升到A *。
表示地图#
研究算法时要做的第一件事是 了解数据。输入是什么?输出是什么?
输入:图形搜索算法(包括 A*)采用"图形"作为输入。图形是一组位置(“节点”)以及它们之间的连接(“边”)。这是我给A*的图表:
具有相同寻路图的不同地图StarRaven的精灵 - 请参阅页脚的链接
A* 看不到任何其他内容。它只能看到图形。它不知道某物是在室内还是室外,或者它是一个房间还是一个门口,或者一个区域有多大。它只能看到图表!它不知道这张地图和这个另一个.
输出:A* 找到的路径为由图形节点和边缘组成.边缘是抽象的数学概念。A 会告诉您从一个位置移动到另一个位置,但它不会告诉您如何。请记住,它对房间或门一无所知;它看到的只是图表。您必须确定 A 返回的图形边缘是否意味着从一个图块移动到另一个图块,或者走直线、打开门、游泳或沿着弯曲的路径跑步。
权衡:对于任何给定的游戏地图,有许多不同的方法可以制作路径查找图以提供给A *。上面的地图使大多数门道进入节点;如果我们做了呢?门道进入边缘?如果我们使用寻路网格?
寻路图不必与游戏地图使用的内容相同。网格游戏地图可以使用非网格寻路图,反之亦然。A* 运行速度最快,图形节点最少;网格通常更易于使用,但会产生大量节点。本页涵盖A *算法,但不包括图形设计;查看我的其他页面有关图表的更多信息。对于本页其余部分的解释,我将使用网格,因为可视化概念更容易。
算法#
有很多算法在图形上运行。我将介绍这些内容:
广度优先搜索在所有方向上都进行了平等的探索。这是一种非常有用的算法,不仅适用于常规路径查找,还可用于程序映射生成、流场路径查找、距离图和其他类型的映射分析。
Dijkstra的算法(也称为统一成本搜索)使我们能够确定要探索的路径的优先级。它不赞成平等地探索所有可能的路径,而是倾向于低成本的路径。我们可以分配更低的成本来鼓励在道路上移动,更高的成本来避免森林,更高的成本来阻止靠近敌人,等等。当移动成本发生变化时,我们使用它而不是广度优先搜索。
A是Dijkstra算法的修改,针对单个目的地进行了优化。Dijkstra的算法可以找到所有位置的路径;A 查找指向一个位置或多个位置中最近的路径。它优先考虑似乎更接近目标的路径。
我将从最简单的广度优先搜索开始,一次添加一个功能以将其转换为A *。
广度优先搜索#
所有这些算法的关键思想是,我们跟踪一个称为前沿的扩展环。在网格上,此过程有时称为"洪水填充",但相同的技术也适用于非网格。启动动画以查看边界如何扩展→→
← 开始动画 →
我们如何实现这一点?重复这些步骤,直到边界为空:
从边界中选取并移除一个位置。→
通过查看其邻居来扩展它。跳过墙壁。我们添加到边界和到达集合的任何未到达的邻居→。
让我们近距离看看这个。磁贴按我们访问它们的顺序进行编号。逐步查看扩展过程:
< 退后一步 向前迈进>
它只有十行(Python)代码:
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*)的本质。但是,我们如何找到最短的路径呢?循环实际上并不构造路径;它只告诉我们如何访问地图上的所有内容。这是因为广度优先搜索不仅可以用于查找路径,还可以用于更多目的。在本文中,我将展示它如何用于塔防,但它也可用于距离图,程序地图生成以及许多其他事情。在这里,尽管我们想使用它来查找路径,因此让我们修改循环以跟踪我们到达的每个位置的来源,并将集合重命名为表(表的键是到达的集合):reachedcame_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
在提前退出的情况下,您可以做很多很酷的事情。
移动成本#
到目前为止,我们已经使步骤具有相同的"成本"。在某些寻路场景中,不同类型的运动有不同的成本。例如,在《文明》中,穿越平原或沙漠可能会花费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
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:
Breadth First Search
Dijkstra’s Algorithm
← 开始动画 →
除 1 以外的移动成本允许我们探索更有趣的图形,而不仅仅是网格。在页面顶部的地图中,移动成本基于房间之间的距离。移动成本也可用于根据与敌人或盟友的距离来避开或偏爱区域。
实现说明:我们希望此优先级队列首先返回最低值。在实现页面上,我在Python中使用显示首先返回最低值,并在C++使用配置为首先返回最低值。此外,Dijkstra的算法和A *我在此页面上介绍的版本与算法教科书中的版本不同。它更接近所谓的统一成本搜索。我在实现页面上描述了差异。PriorityQueueheapqstd::priority_queue
启发式搜索#
通过广度优先搜索和Dijkstra算法,前沿向各个方向扩展。如果您尝试查找指向所有位置或多个位置的路径,这是一个合理的选择。但是,一种常见情况是仅查找指向一个位置的路径。让我们让边界向目标扩展,而不是向其他方向扩展。首先,我们将定义一个启发式函数,告诉我们离目标有多近:
def heuristic(a, b):
return abs(a.x - b.x) + abs(a.y - b.y)
在Dijkstra的算法中,我们使用从开始的实际距离进行优先级队列排序。在这里,在贪婪的最佳首次搜索中,我们将使用到目标的估计距离进行优先级队列排序。将首先探索最接近目标的位置。该代码使用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的算法可以很好地找到最短的路径,但它浪费了时间探索那些没有希望的方向。贪婪的最佳第一搜索探索有希望的方向,但它可能找不到最短的路径。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的算法计算从起点到起点的距离。贪婪的最佳搜索会估计到目标点的距离。A* 使用这两个距离的总和。
迪克斯特拉算法
贪婪的最好第一
A* 搜索
尝试在各个地方在墙上打开一个洞。你会发现,当贪婪的最佳搜索找到正确的答案时,A *也会找到它,探索同一区域。当贪婪的最佳搜索找到错误的答案(更长的路径)时,A *会找到正确的答案,就像Dijkstra的算法一样,但仍然比Dijkstra的算法探索得更少。
A* 是两全其美的。只要启发式算法不会高估距离,A* 就会找到一条最佳路径,就像 Dijkstra 的算法一样。A* 使用启发式方法对节点重新排序,以便更有可能更快地遇到目标节点。
和。。。就是这样!这就是 A* 算法。
更多#
您准备好实现它了吗?请考虑使用现有库。如果您自己实现它,我有配套指南,逐步展示了如何在Python,C++和C#中实现图形,队列和寻路算法。
您应该使用哪种算法在游戏地图上查找路径?
如果要查找来自或指向所有位置的所有位置的路径,请使用广度优先搜索或 Dijkstra 算法。如果移动成本都相同,请使用广度优先搜索;如果移动成本不同,请使用Dijkstra算法。
如果要查找指向一个位置或多个目标中最接近的路径,请使用 Greedy 最佳首次搜索或 A*。在大多数情况下,首选 A*。当您想要使用贪婪的最佳首次搜索时,请考虑使用带有"不允许的"启发式方法的A 。.
最佳路径呢?广度优先搜索和Dijkstra算法保证在给定输入图的情况下找到最短路径。贪婪的最佳首次搜索不是。如果启发式方法从未大于真实距离,则保证 A 能找到最短路径。随着启发式算法变小,A变成了Dijkstra算法。随着启发式方法变大,A 变为贪婪的最佳首次搜索。
性能如何?最好的办法是消除图表中不必要的位置。如果使用网格,请参阅此。减小图形的大小有助于所有图形搜索算法。之后,使用最简单的算法;更简单的队列运行得更快。贪婪的最佳首次搜索通常比Dijkstra的算法运行得更快,但不会产生最佳路径。A*是大多数寻路需求的不错选择。
非地图呢?我在这里显示地图,因为我认为使用地图更容易理解算法的工作原理。但是,这些图形搜索算法可以用于任何类型的图形,而不仅仅是游戏地图,并且我试图以独立于2d网格的方式呈现算法代码。地图上的移动成本将成为图形边缘上的任意权重。启发式方法不容易转换为任意地图;你必须为每种类型的图形设计一个启发式。对于平面地图,距离是一个不错的选择,所以这就是我在这里使用的。
我在这里写了更多关于寻路的文章.请记住,图表搜索只是您需要的一部分。A*本身不处理诸如合作移动,移动障碍物,地图更改,危险区域评估,编队,转弯半径,对象大小,动画,路径平滑或许多其他主题之类的事情。
原文地址 https://www.redblobgames.com/pathfinding/a-star/introduction.html