A*寻路算法的探寻与改良(一)

A*寻路算法的探寻与改良(一)

by:田宇轩                                                                   

第一部分:这里我们主要探讨A*算法的基本概念和原理,对A*算法有一定了解的朋友们可以跳过并阅读稍后更新的其他部分

1.1 前言

这篇文章原来属于我的数据结构课设内容,这学期的数据结构和算法学习让我仿佛打开了新世界的大门,让我意识到优化美学和设计代码是这么有趣的事情,这是一种在编程语言之上抽象力量。因此我准备在博客园长期更新一些自己的心得体会和经验,以前也没有记笔记的习惯,正好用这种形式近期更新一些数据结构和算法的文章,这篇文章也是我第一次在博客园发布的文章,语言逻辑之间很多不妥之处,还请各位指出校正。您可以通过我的邮箱591223738@qq.com联系我,欢迎大家随时批评指教,感谢大家的关注。

1.2 A*算法要解决的问题

我们可以通过先了解A*算法解决的问题来了解A*算法的概念。A*是一种用于寻路的算法,现在我们给计算机一张地图,标记了哪些地方能走,哪些地方不能走,然后告诉计算机起点A和终点B,计算机自动计算出一条从A点到B点的最短路径,我们的问题就解决了。A*算法就是用于快速解决这类问题的方法。

让我们先用抽象的方法看这个问题,首先,解决这类问题的前提条件有以下两个:

第一个条件是,地图必须是事先设计好的(这里重点内容会用黑体mark一下,后同)。也就是说地形、各点的位置等地图的属性是固定不变的,这种地图不能前一秒里面还有一条河,后一秒同样位置就没有这条河了。本着抽象的原则,我们把这种有固定属性的地图叫做静态路网。因此,A*是一种基于静态路网的寻径算法。

使用A*的第二个条件是,已知起点和终点。在一些RPG游戏中,经常会有游戏迷宫这种设定,在游戏迷宫中,玩家只能确定自己的位置(仙剑3的一些迷宫里,因为有传送的概念,玩家甚至不能确定自己的位置),但是玩家并不知道迷宫的终点在哪里,这种情况下用A*算法是不合适的。因此,A*算法需要知道起点和终点才能求出最优路径。

符合这两个条件的问题,就是我们可以尝试用A*算法解决的问题。和你预料的一样,A*算法被广泛应用在游戏编程之中。

1.3 A*算法的寻路思路

1.3.1 设定和定义地图、地貌、起点和终点

了解了A*算法的概念之后,我们来看看A*算法是怎么解决寻路这个问题的。

首先,我们给计算机一张地图的同时,我们也要给计算机一个处理地图的方式。在A*中,处理地图的思想就是让计算机把一张大地图看成是由大小相等的格子拼出来的结构,这些格子都不可再分割成更小的格子,它们是地图上的最小完整单位。我们事先把每个格子的状态在电脑中标记起来,比如这个格子的地形不可通过,我们就打上一种标签;如果这个格子能通过,但是由于现实地形的原因(比如,这条路比较坎坷),这个格子就被打上另一种标签(比如:”难以通过的格子”);如果这个格子对应的地形一马平川,这个格子的标签可以换成"可以通过的格子"。

好的,读到这里,我们大概能知道地图是怎么在A*算法中被处理的了,还是基于抽象的思想,我们可以得出重要的两种结构:

1.地图

地图由有各种属性的格子组成。

2.格子

      格子的基本属性包括以下两点:

(1).格子在地图中的位置,即坐标。要保证现有的格子足以描述一片完整的区域,因此这个坐标应该是连续的而非离散的。

(2).格子的“路况”,就是上面我们说的那些标签,不同的标签代表不同的格子内路况,每次寻路如果格子内相对路况更差,我们就会尽量绕开它,其实路况有很多种,这么多种类用标签分类,其实是不太靠谱的,这里我们直接量化这些不同的格子内路况,比如地形越难通过,相应地就设定一个更大的值(比如,999),地形越容易通过,就设一个更小的值。在A*算法里,我们把这种值叫做“权”,或者权值,它量化地直观体现了每个格子的路况。

在设定好地图组成的基础上,只要再设定好哪个格子是起点,哪个格子是终点,我们就可以考虑怎么找一条最优路径了。

1.3.2 BFS(Breadth First Search,广度优先搜索)和DFS(Depth-First-Search,深度优先搜索算法

在思考怎么寻找最优路径之前,应该先定义什么叫做最优路径,最优路径不只是A与B的距离最短,还要求我们权衡经过的路径权值尽量的低,权值越低的路径就意味着越适合的路线。这里,我们把起点到终点经过格子用的长度和一路格子的权值之和一起考虑,我们设这种衡量路线是否最优的标准叫“代价”,计算最优路径,其实就是计算代价最小的路径。因此,A*算法的重点就是如何评估代价。

对于从A点到B点的寻路方法,可能最容易被想出来的就是,列举出所有从A到B的路径,并逐个计算每个路径需要的代价,量化代价值,挨个比较,这样就很容易看哪个路径最短了,其实这是种“笨方法”,因为可能地图比较大,格子比较多,起点A到终点B距离很远,所以逐一列举出来A到B的路线,逐一计算显然需要很高的时间/空间复杂度(对于数据结构中的一些百度一下就能理解的基础概念,这里用深绿色标明,本文不加解释,下同),不是好的算法。这有点像网络安全中的暴力破解,一个密码锁有三位数,000~999,挨个数尝试解锁最多用999次不断尝试,就能知道正确密码,但这种方法显然不如社工密码锁主人或者用生日什么的做线索数字去试高效。

虽然这种逐步寻径的方法并不高效,但是A*算法的思路融合了这两种方法,因此,在学习A*之前掌握BFS和DFS还是很重要的。BFS以起点A为圆心,先搜索A周围的所有点,形成一个类似圆的搜索区域,再扩大搜索半径,进一步搜索其它没搜索到的区域,直到终点B进入搜索区域内被找到。整个搜索过程就像下面的图1.3.2.1一样。

A*寻路算法的探寻与改良(一)

图1.3.2.1—BFS搜索终点

图中,粉色的点是起点A,紫色的点是终点B,颜色为灰色的色块代表未检索区域,蓝色/蓝黑色的色块代表已经检索了的区域,颜色越黑的色块代表地图上越先被BFS搜索的格子。从图中我们可以看出,BFS尽可能把更多的格子纳入搜索区域,并总是能找到A与B之间的最佳路径(如果存在这么一条路径的话)。但是DFS寻找终点的思路就和BFS不同,BFS尽量遍历A周围的区域,DFS则是让搜索的区域离A尽量远,离B尽量近,比如现在你在一个陌生的大学校园里,你知道校门口在你的北方,虽然你不知道你和校门口之间的路况如何,地形如何,但是你会尽可能的往北方走,总能找到校门口。DFS的搜索过程就像下图1.3.2.2这样:

A*寻路算法的探寻与改良(一)

图1.3.2.2—DFS搜索过程

可以清楚地对比出来,比起BFS,DFS因为尽量靠近终点的原则,其实是用终点相对与当前点的方向为导向,所以有一个大致的方向,就不用盲目地去找了,这样,就能比BFS能快地找出来最短路径,但是这种快速寻找默认起点A终点B之间没有任何障碍物,地形的权值也都差不多。如果起点终点之间有障碍物,那么DFS就会出现绕弯的情况。见图1.3.2.4:

A*寻路算法的探寻与改良(一)

图1.3.2.4—DFS一直向右下角搜索,但由于被挡住去路,因只能绕了一个大圈

图中DFS算法使电脑一路往更右下方的区域探索,可以看出,在DFS遇到障碍物时,其实没有办法找到一条最优的路径,只能保证DFS会提供其中的一条路径()如果有的话。

大概了解了BFS和DFS,对比这两者可以看出来,BFS保证的是从起点到达路线上的任意点花费的代价最小(但是不考虑这个过程是否要搜索很多格子)DFS保证的是通过不断矫正行走方向和终点的方向的关系,使发现终点要搜索的格子更少(但是不考虑这个过程是否绕远)。

     A*算法的设计同时融合了BFS和DFS的优势,既考虑到了从起点通过当前路线的代价(保证了不会绕路),又不断的计算当前路线方向是否更趋近终点的方向(保证了不会搜索很多图块),是一种静态路网中最有效的直接搜索算法(会在稍后的文章中提到不直接搜索地图的寻路方式)。

1.3.3 A*算法的估价思想

     计算机上的事物,一定是先要量化,然后再计算的,这是一种抽象的过程。在BFS中,越接近起点的格子的代价越低,越远离起点的格子的代价越高。每次搜索,都是在尚未搜索的范围中寻找他们之间更接近起点的格子来进行搜索;在DFS中,越接近终点的格子代价越低,离终点越远的格子的代价越高。如果要融合这两种方法,我们应该怎么对每个格子估值呢?

在A*中,每个格子的估值(估值了这个格子代价)是由两部分组成的,一部分是这个格子距离起点的距离,我们记作G,另一部分是这个格子距离终点的距离,这种距离,我们记作H。注意,这里的距离不是指直线距离,G指的是从这个格子的起点到达这个格子一共花费的距离,H指的是估算一下从这个格子到终点将会用多少距离,我们可以看出来,我们事先并不知道这个格子到终点的花费,因此,我们必须找一种方法估算当前点与终点的距离,这种估算的方法,我们称为启发式算法,A*的估价思想就是用量化G值和F值(其实就是求出每个要搜索格子的这两个值),然后加在一起,它们的和就是这个格子的估值(或者叫做代价),这里我们把他们的和记作F。很好,我们现在就可以得出A*算法中计算格子代价的公式:F=G+H.F,G,H也都是格子的属性,让我们把这三个参数添加到格子的性质中去。

     通过这个公式,求出搜索的每个格子的权值,然后每次都选择代价更小的格子,这样,我们就能得出一条最佳路径。这就是A*算法的估价思想。

1.4 A*算法的描述

以下是对A*算法的完整概括:

1,找到起点对应的格子,并把起始格添加到开启列表里。开启列表是一个用于记载我们待处理格子的列表。(蓝色用于写注释,下同)

2,重复如下的工作:

a) 寻找开启列表中F值最低的格子。我们称它为当前格

b) 把当前格从开启列表中删除,放到关闭列表里面。关闭列表是一个用于记载我们已经处理过的处理格子的列表。

c) 对当前格的每一个相邻格子,分三种情况进行操作:

* 如果它不可通过或者已经在关闭列表中,略过它。反之如下。

* 如果它不在开启列表中,把它添加进去。把当前格作为它的父节点。记录这一格的F,G,和H值。父节点用于记载当前格子是哪个格子的相邻格。

* 如果它已经在开启列表中,就说明我们之前已经访问过了这个格子,现在我们又遇到了它,说明可以通过这个格子的父节点直接到达当前格,这种情况下进一步说明至少有两种方式到达这个格子,因此我们必须知道哪种方式到达这一格更好,才能进行下一步操作。

为了知道哪种方式更好,我们假设当前点是这个格子的父节点,然后计算这个节点的G值,然后比较我们刚计算出来的G值是否比原来的G值(因为这个格子如果在开启列表,就说明已经有了父节点和求好的G值),更低的G值意味着更好的路径。如果是这样,就把这一格的父节点改成当前格,并且重新计算这一格的G和F值。

d) 停止,当你

* 把目标格添加进了关闭列表,这时候路径被找到,或者

* 没有找到目标格,开启列表已经空了。这时候,路径不存在。

这就是今天归纳出的A*算法的寻路过程,在下一篇文章中,我们将用C语言实现它,并进一步阐释A*算法的执行逻辑,如果大家不会用C语言,这没有关系,因为我们将用逻辑和中文伪代码先设计和构建A*算法,然后再写具体的代码,即使你不会C语言,你也可以对照伪代码,用其他语言实现。敬请期待。如果您有疑问或者其他观点,请联系我,我将很快进行答复。

你可以点击这里查看第二章的内容。

上一篇:A*寻路算法的探寻与改良(二)


下一篇:A*寻路算法lua实现