利用Red Blob游戏介绍A*算法

转自:http://gad.qq.com/program/translateview/7194337

在游戏中,我们经常想要找到从一个位置到另一个位置的路径。我们不只是想要找到最短距离,同时也要考虑旅行时间。如下图中从星星(起点)到叉号(终点)的最短路径。

为了找到这样的路径,我们可以使用一种图搜索算法,它需要将地图表示为一张图。A *算法是图形搜索的热门选择。宽度优先搜索是图形搜索算法中最简单的一种,所以让我们从这个算法开始,慢慢扩展到A*算法。

地图的表示

研究算法时,首先要了解数据。输入是什么?输出是什么?

输入:图形搜索算法(包括A *)都是以“图形”为输入。一个图就是一组的位置(“点”)和他们之间的连接关系(“边”)。下面就是A*算法的输入:

A*算法不会在意其他任意的东西,它只能理解这样的图。它不会知道某个东西是在室内还是户外,它是一个房间还是一个门,或者它的面积有多大。它只

能看懂图中的点和线。它也不会知道上一幅图和下一幅图的区别。

输出: A *找到的路径是由图中节点和边组成(如上图)。边是抽象的数学概念。A *能够告诉你从一个位置移动到另一个位置的路径,但不会告诉你怎么移动。注意:它不知道任何关于房间或门的信息,它只知道图形中的点和线。你必须自己决定A *返回的图形路线是从Tile移动到Tile,还是以直线行走或是打开门,或者沿着弯曲的路径游泳或跑步。

权衡:对于任意给定的游戏地图,都有许多不同的方法来制作寻路图给A *。上述地图大多数使用门做为点,如果我们把门道作为边呢?如果我们使用寻路网格呢?

以门道为边

寻路网格

寻路图并不是必须和你使用的游戏地图相同。网格游戏地图可以使用非网格寻路图,反之亦然。图中的点越少A*运行速度就越快。通常网格在使用时更容易,但会产生大量节点。本文主要讲解A*算法,并不包括图形设计; 有关图形的更多信息,请参阅我的其他文章。文章剩余部分的讲解中,我将使用网格,因为它更容易将概念可视化。

算法

在图上寻找路径有很多的算法。我要介绍下面这些:

度优先搜索在各个方向上都是相同的。这是一个非常有用的算法,不仅适用于常规路径查找,还可用于过程图生成,流域寻路,距离图和其他类型的地图分析。

Dijkstras算法(也称为统一成本搜索-UniformCost Search)我们会优先考虑对哪些路径进行探索,而不是平等地探索所有可能的路径,它有利于寻找花费代价更小的路径。我们可以分配较低的代价来鼓励在道路上移动、更高的代价来避免森林、更高的代价阻止靠近敌人等等。当图中移动代价是变化时,我们使用这种方式而不是宽度优先搜索。

A *是Dijkstra’s算法的变形,为单一目的地做了优化。Dijkstra’s的算法可以找到通往任意位置的路径; A *是为了找到通往一个位置的路径。它优先考虑似乎更接近目标的路径。

我将从最简单的“宽度优先搜索”开始讲解,并一次添加一个功能逐渐将其转换为A *

宽度优先搜索

所有这些算法的关键思想是我们持续跟踪一个称为frontier的扩展环。在网格上,这个过程有时被称为“flood fill”,但这个的技术同样适用于非网格图。点击开始,看看frontier是如何扩展:(点击查看原文动图)下面只是一个截图

这个过程我们如何实现呢?重复这些步骤直到frontier为空:

1、从frontier选择并移除一个位置。

2、将该位置标记为已访问,以便我们知道不用再处理它。

3、通过查看其邻居来扩展 frontier。将所有的还没有访问过的的邻居,加入到frontier上。

我们来看看整个过程。Tile会按照我们访问的顺序进行编号。

逐步浏览扩展过程:(点击查看原文动图)下面只是截图

它只有十行(Python)代码:

frontier = Queue()
frontier.put(start )
visited = {}
visited[start] = True
while not frontier.empty():
   current = frontier.get()
   for next in graph.neighbors(current):
      if next not in visited:
         frontier.put(next)
         visited[next] = True

这个循环是本文图形路径搜索算法的精髓,包括对于A *算法而言。但是我们如何找到最短的路径?事实上这个循环并不能构造出路,它只是告诉了我们如何访问地图上的所有事物。这是因为宽度优先搜索的作用不仅仅是用来寻找路径,我在在另一篇文章中介绍了如何在塔防游戏中使用这个算法,但它也可以用于地图测距,程序地图生成和许多其他事情。在这里我们使用它来寻找路径,所以我们对循环进行修改使之能够从我们访问过的每个位置追踪到我们的出发点,并重命名visited为came_from:

frontier = Queue()
frontier.put(start )
came_from = {}
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 = [current]
while current != start:
   current = came_from[current]
   path.append(current)
path.append(start) # optional
path.reverse() # optional

这是最简单的寻路算法。正如上面的展示,它不仅可以在网格上工作,而是对任何形式的图形结构都有效。在地牢中,图中的点可以是房间,图中的边将各个门口连接起来。在平台中,图中的点可以是位置,图中的边是可能的动作,例如向左移动,向右移动,向上跳,向下跳。总的来说,将图视为状态和改变状态的动作。我在其他文章中写了更多的代表性的地图。在本文的其余部分,我将继续使用网格图形作为例子,并探索为什么你可能需要使用宽度优先搜索的变体。

提前退出

我们已经能够找到从一个位置到所有其他位置的路径。但是通常我们并不需要所有的路径; 我们只需从一个位置到另外一个位置的一条路径。一旦我们找到目标,我们就可以停止扩大Frontier。在下面的图中拖动X,看看在到达X时Frontier如何停止扩展。(点击链接进行操作)

代码很简单:

frontier = Queue()
frontier.put(start )
came_from = {}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’s算法(也称为统一成本搜索)。它与宽度优先搜索的不同是什么呢?Dijkstra’s算法中我们需要跟踪运动成本,所以让我们添加一个新变量cost_so_far来跟踪从起始位置开始的总运动成本。我们在决定如何评估一个位置时要将运动成本纳入考虑,我们把队列变成一个优先队列。隐含的是我们可能会多次访问同一个地点但基于不同成本,所以我们需要轻微的改变一下算法逻辑。如果一个位置从未被访问过,就把它加入Frontier,如果到达该位置的新路径优于前面的最佳路径,更新它在Frontier中的值。

frontier = PriorityQueue()
frontier.put(start, 0)
came_from = {}
cost_so_far = {}
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

这里我们使用优先级队列替代普通队列来改变Frontier扩展的方式。轮廓线会很好的表明这一点。通过动画就能看出边界在通过森林时会扩展的很慢,将会找到一条围绕森林的最短路径,而不是穿过它:(点击查看原文动画)

超过1的运动成本使我们能够探索更有趣的图形,而不仅仅是网格。在本文开始介绍的地图上,运动成本用是基于房间到房间的距离。运动成本也可以用来避免或偏好邻近敌人或贴近盟友的区域。

实现细节:常规优先级队列支持插入和删除操作,但Dijkstra’s算法的一些演示文稿还使用第三个操作来修改已经在优先级队列中的元素的优先级。我没有用该操作,原因在实现说明页面有讲述。

启发式搜索

利用宽度优先搜索和Dijkstra’s算法,边界可以在所有的方向扩展。如果你尝试找到到达所有地点或许多地点的路径,这会是一个合理的选择。然而,常见的情况是只需要找到到达某一个位置的路径。这需要让边界的扩展朝着目标的方向,而不是向四周扩展。首先,我们将定义一个 heuristic函数,来明确我们与目标之间的距离:

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

在Dijkstra’s算法中,我们使用了距离起点的实际距离的优先级队列排序。相反,在Greedy BestFirst Search算法中,我们将使用到目标的评估距离作为优先级队列的排序依据。最接近目标的位置会最先被探索。下面的代码使用了宽度优先搜索中的优先级队列,但但没有使用Dijkstra算法中的cost_so_far方法:

frontier = PriorityQueue()
frontier.put(start, 0)
came_from = {}
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’s算法可以很好的找到最短路径,但是会在没有价值的方向上花费时间去探索。GreedyBest First Search 算法只在正确的方向上探索,但它可能找不到最短的路径。A *算法中同时使用了从起点的实际距离和到目标的估计距离。

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

frontier = PriorityQueue()
frontier.put(start, 0)
came_from = {}
cost_so_far = {}
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’s算法计算距离起点的距离。Greedy Best-First Search估计到目标点的距离。A *使用这两个距离的总和。(点击查看原图)

算法会尝试下在墙(深色的格子)上打一个洞(点击链接,查看动画效果)。你会发现探索同一个地区,当Greedy Best-First Search算法找到正确的答案时,A *也能发现它。当Greedy Best-First Search算法得到错误的答案(较长的路径)时,A*还是可以找到正确的答案,就像Dijkstra’s的算法,但比Dijkstra’s的算法探索的少。

A *是二者的完美结合。只要heuristic函数不高估距离,A *就不会使用heuristic函数得到一个近似的答案。它找到一个最佳的路径,就像Dijkstra’s’s算法一样。A *使用 heuristic函数重新排序节点,这样可以让它更早的遇到目标节点。

好吧...就是这样!这就是A *算法。

补充

你准备好实现它了吗?考虑使用现有的库吧。如果你准备自己实现它,我有配套指南,逐步展示如何在Python,C ++和C#中实现图形,队列和寻路算法。

你应该使用哪种算法来在游戏地图上查找路径呢?

如果要找到通往所有位置的路径,请使用宽度优先搜索或Dijkstra’s算法。运动成本相同时,使用宽度优先搜索; 运动成本不同,则使用Dijkstra’s算法。

如果您想查找到某一个位置的路径,请使用Greedy BestFirst Search或A *算法。大多数情况下A *会表现更好。所以当你试图使用Greedy Best First Search算法时,请考虑使用“inadmissible” heuristic的 A* 算法。

如何得到最佳路径呢?宽度优先搜索和Dijkstra’s算法保证找到基于给定输入图的最短路径。而Greedy BestFirst Search算法做不到。如果heuristic函数得到的距离永远小于真实距离,则A *算法保证找到最短路径。随着heuristic函数距离变小,A *算法逐渐变成Dijkstra’s的算法。随着heuristic函数距离变得越来越大,A *算法会变成Greedy Best First Search算法。

如何提升算法性能呢?最好的办法是消除图中不必要的位置。如果使用网格图,请参阅。缩小图形大小对所有的图形搜索算法都是有益的。此外,尽可能的使用最简单的算法; 最简单的队列,算法的运行速度才会更快。Greedy Best First Search算法通常比Dijkstra’s算法运行速度更快,但它不能产生最佳路径。A *是大多数寻路问题的好选择。

如果图形不是地图呢?我在这里用地图举例,因为我认为通过使用地图更容易了解算法的工作原理。实际上,这些图形搜索算法可以用于任何类型的图形,而不仅仅是游戏地图,并且我已经尝试以独立于2d网格的方式呈现算法代码。地图上的运动成本成为图形边缘上的任意权重。heuristics函数并不能轻易地转换到任意地图上,你必须为每种类型的图形设计对应的heuristics函数。对于平面图,距离是一个不错的选择,这就是我在这里使用它的原因。

在这里写了很多关于寻路的文章。请记住,图形搜索只是你需要的一部分。A *算法本身不处理合作运动,移动障碍,地图变更,危险区域评估,编队,转弯半径,物体大小,动画,路径平滑等很多其他话题。

时间: 2024-10-12 03:22:20

利用Red Blob游戏介绍A*算法的相关文章

四舍五入的一些简单写法(利用内置函数,算法2种写法)

?       //内置函数的写法        //网上零售价和折扣价在计算结束需要进行进位,规则如下:         //个位为1,2,3,4进位到5,例如计算后的价格为1201,则价格为1205:         //个位为6,7,8,9进位到0,例如计算后的价格为1209.则价格为1210:         public static string ChangePrice(double price)         {             int changed = 0;     

利用Python实现 队列的算法

以下内容都是来自"悟空"大神的讲解,听他的视频课,利用Python实现堆栈的算法,自己做了一些小总结,可能会存在自己理解的一些误区, 1.栈的实现 队列的特征是先进先出,同我们生活中的队列具有相同的特性:先排队的先办事.队列的应用场景非常多,例如消息通信.多进程间的协同.多线程间的协同等. 在算法前,我们需要先知道3个值  1.节点(节点作用是 进行一个指针(假设的)一个指向  2.就是现在这个位置上的元素的值)  2.还需要知道一个头,一个尾,就如上面两个一个head,就对应下面的代

【程序员的吃鸡大法】利用OCR文字识别+百度算法搜索,玩转冲顶大会、百万英雄、芝士超人等答题赢奖金游戏

[先上一张效果图]: 一.原理: 其实原理很简单: 1.手机投屏到电脑: 2.截取投屏画面的题目部分,进行识别,得到题目和三个答案: 3.将答案按照一定的算法,进行搜索,得出推荐答案: 4.添加了一些其他辅助功能,比如:浏览器搜索结果展示.关键字高亮.浏览器可点击等: 二.二营长,把我的意大利...............代码,呈上来,给友军看看 1.手机投屏: 方式很多,这里只列举几个比较常用.且自己感觉简单易用的: A.IOS:局域网内,可以利用iTools里的苹果录屏大师(airplay)

【翻译】利用加速度求解位置的算法——三轴传感器

摘要 此文档描述并使用MMA7260QT三轴加速计和低功耗的9S08QG8八位单片机实现求解位置的算法 . 在今天先进的电子市场,有不少增加了许多特性和智能的多功能的产品.定位和游戏只是得益于获取到的位置信息的一部分市场.一个获取这种信息的可选方案是通过使用惯性传感器.从这些传感器中取得的信号需要进行一些处理,因为在加速度和位置之间没有一种直接转换. 为了获得位置,需要对加速度进行二次积分.本文介绍一种简单的算法实现加速度的二重积分.为了获取加速度的二重积分,一个简单的积分要进行两次,因为这样也

Java利用MessageDigest提供的MD5算法加密字符串或文件

MD5是常用的加密算法,也经常用于校验信息完整,如文件的完整性.用术语讲,MD5是一种消息摘要算法(Message Digest Algorithm).另外还有一种常用的消息摘要算法SHA1.如果想了解这些的话,可以去百度百科:MD5.SHA1.消息摘要算法. Java已经实现了MD5.SHA1算法.利用java.security.MessageDigest类就可以获取字符串和文件的MD5以及SHA1结果. 1.字符串的MD5(下面的代码有详细注释) public static String s

利用栈实现逆波兰算法

1.逆波兰表达式? 在我们的普遍认知中,计算的优先级总是和()相关,形如(1+2)*(3+4)这样的式子,我们看起来十分的清晰明了,但对计算机来说,它会进行很多次的判断来确定一个运算的优先级.于是在很久很久之前就有一个人发现,如果我们将上述算式写成形如1 2 + 3 4 + *的形式,计算机判断起来会显得格外的快,效率也会更高,然而它的实现原理是什么样的呢. 2.算法分析 经过观察,我们发现该算式将参数放在前面,运算操作符放在两个要进行计算的参数之后,我们可以得出这样的思路:每次我们将拿到的数据

机器学习算法( 二、K - 近邻算法)

一.概述 k-近邻算法采用测量不同特征值之间的距离方法进行分类. 工作原理:首先有一个样本数据集合(训练样本集),并且样本数据集合中每条数据都存在标签(分类),即我们知道样本数据中每一条数据与所属分类的对应关系,输入没有标签的数据之后,将新数据的每个特征与样本集的数据对应的特征进行比较(欧式距离运算),然后算出新数据与样本集中特征最相似(最近邻)的数据的分类标签,一般我们选择样本数据集中前k个最相似的数据,然后再从k个数据集中选出出现分类最多的分类作为新数据的分类. 二.优缺点 优点:精度高.对

相似图片搜索的三种哈希算法

想必大家都用google或baidu的识图功能,上面就是我搜索冠希哥一幅图片的结果,达到图片比较目的且利用信息指纹比较有三种算法,这些算法都很易懂,下面分别介绍一下: 一.平均哈希算法(aHash) 此算法是基于比较灰度图每个像素与平均值来实现的,最适用于缩略图,放大图搜索. 步骤: 1.缩放图片:为了保留结构去掉细节,去除大小.横纵比的差异,把图片统一缩放到8*8,共64个像素的图片. 2.转化为灰度图:把缩放后的图片转化为256阶的灰度图. 附上灰度图相关算法(R = red, G = gr

支持向量机(SVM)算法

支持向量机(support vector machine)是一种分类算法,通过寻求结构化风险最小来提高学习机泛化能力,实现经验风险和置信范围的最小化,从而达到在统计样本量较少的情况下,亦能获得良好统计规律的目的.通俗来讲,它是一种二类分类模型,其基本模型定义为特征空间上的间隔最大的线性分类器,即支持向量机的学习策略便是间隔最大化,最终可转化为一个凸二次规划问题的求解. 具体原理: 1. 在n维空间中找到一个分类超平面,将空间上的点分类.如下图是线性分类的例子. 2. 一般而言,一个点距离超平面的