最适合入门者的A*(A星)算法详解

A*搜寻算法俗称A星算法。这是一种在图形平面上,有多个节点的路径,求出最低通过成本的算法。常用于游戏中的NPC的移动计算,或线上游戏的BOT的移动计算上。

想了解之前树根讲的另一种智能算法,可以戳开:一文从了解遗传算法到用遗传算法玩拼图

1

Dijkstra 算法

 A-star Algorithm

最短路径是图论算法中的经典问题。图分为有向图、无向图;路径权值有正值、负值,针对不同的情况需要分别选用不同的算法。

针对无向图,正权值路径,最典型的做法是采取Dijkstra算法。

Dijkstra算法的原理不难理解,但是假如用文字表达会增大初学者的理解难度,所以树根直接上图:

下面以图为例看一下具体流程:

其中箭头边的数值代表两点之间的距离。

我们假设从点1出发,计算出点1到点8的最短路径。我们可以看一下Dijkstra 算法是怎么求出点1到点8的最短距离的(其中S表示已求出最短路径的点的集合):

(1)点1直接到点4的距离最短,所以点1到点4的距离 P4=1,同时点4加入S集合:

(2)显而易见,点1直接到点2的距离是最短的,为2,因此距离P2=2,点2加入集合S中:

(3)同理,点1直接到点6的距离是最短的,P6=3,点6加入S:

(4)点1到点7的最短路径为:1 -> 4 -> 7,距离P7=1+2=3,点7加入S:

(5)点1到点5的最短路径为:1-> 4->7->5,此时距离P5=6,把点5加入S:

同理遍历所有节点:

这样我们就可以得到点到所有节点的最短路径,可以得到点1到点8的最短路径为:1->4->7->5->8,P8=10

因此,计算任意两点的最短距离时,Dijkstra算法每一步会选择离初始点最近的结点。

还有没有其他的方法呢?

2

最佳优先搜索(BFS)

A-Star Algorithm

最佳优先搜索(BFS)算法按照类似的流程运行,不同的是它能够评估(称为启发式)任意结点到目标点的代价。

与选择离初始结点最近的结点不同的是BFS选择离目标点最近的结点。

BFS不能保证找到一条最短路径。然而,它比Dijkstra算法快的多,因为它用了一个启发式函数(heuristic function)快速地导向目标结点。例如,如果目标位于出发点的南方,BFS将趋向于导向南方的路径。

在下面的图中,颜色越浅的结点代表越高的启发式值(移动到目标的代价高),而越黑的结点代表越低的启发式值(移动到目标的代价低)。这表明了与Dijkstra 算法相比,BFS运行得更快,但是在存在障碍物的情况下,它找到的路径可能不是一条好的路径:

问题在于,BFS是基于贪心策略的,它试图向目标点移动,尽管这不是正确的路径。由于它仅仅考虑到达目标的代价,而忽略了当前已花费的代价,于是尽管路径变得很长,它仍然继续走下去。

假如能结合两者的优点不是更好吗?

3

A* (A星)算法

A-Star Algorithm

让我们想象一下,有一款游戏,游戏中一只猫想要找到获取骨头的最短路线:

不幸的是,猫不能直接从它当前的位置走到骨头的位置,因为有面墙挡住了去路。

一、简化搜索区域

寻路的第一步是简化成容易控制的搜索区域。

作为代替,我们使用方块(一个正方形)作为寻路算法的单元。其他的形状类型也是可能的(比如三角形或者六边形),但是正方形是最简单并且最适合我们需求的。

现在让我们基于目前的区域,把区域划分成多个方块来代表搜索空间(在这个简单的例子中,7*6个方块 = 42 个方块):

二、Open和Closed列表

我们的猫没有好的记忆力,所以它需要两个列表:

一个记录下所有被考虑来寻找最短路径的方块的列表(称为open 列表)

一个记录下不会再被考虑的方块的列表(称为closed列表)

猫首先在closed列表中添加当前位置(我们把这个开始点称为点 “A”)。然后,把所有与它当前位置相邻的可通行小方块添加到open列表中。

下图是猫在某一位置时的情景(绿色代表open列表):

在A星寻路算法中,通过给每一个方块一个值 F,该值被称为路径增量。让我们看下它的工作原理!

三、路径增量

我们将会给每个方块一个和值:

F=G+H

G:从开始点A到当前方块的移动量(可以与Dijkstra算法联系起来理解)。所以从开始点A到相邻小方块的移动量为1,该值会随着离开始点越来越远而增大。

H:从当前方块到目标点(可以与BFS算法联系起来理解)的移动量估算值。这个常被称为探视,因为存在障碍物,我们不确定实际的移动量是多少 , 仅仅是一个估算值。

你也许会对“移动量”感兴趣。在游戏中,这个概念很简单 – 仅仅是方块的数量。

然而,在游戏中你可以对这个值做调整。例如:

·如果你允许对角线移动,你可以针对对角线移动把移动量调得大一点(因为存在根号会增大计算量)。

·如果你有不同的地形,你可以将相应的移动量调整得大一点 。

现在让我们详细分析下如何计算出G和H值。

关于G值:

G是从开始点A到达当前方块的移动量(在本游戏中是指方块的数目)。

为了计算出G的值,我们需要从它的上一个方块获取,然后加1。所以,每个方块的G值代表了从初始点A到该方块所形成路径的总移动量

例如,下图展示了两条到达不同骨头的路径,每个方块都标有它的G值(我们现在只允许前后左右移动,不允许对角线移动):

关于H值:

H值是从当前方块到终点的移动量估算值(在本游戏中是指方块的数目)。移动量估算值离真实值越接近,最终的路径会更加精确。

为了让它更简单,我们将使用“曼哈顿距离方法”,它只是计算出距离目标点B剩下的水平和垂直的方块数量,同时略去了障碍物或者不同陆地类型的数量(我们在估算距离的时候往往不确定前方会有怎么样的障碍物)。

例如,下图展示了使用“曼哈顿距离”,从不同的开始点到终点,去估算H的值(黑色数字表示H值):

四、猫的路径

在下图中,和大家说一下每个方块图例的意思:

F(方块的和值):左上角的数字

G(从A点到方块的移动量):左下角的数字

H(从方块到B点的估算移动量): 右下角的数字

箭头指示了到达相应方块的移动方向。

红色方块表示closed列表,绿色方块表示open列表。

好的,我们开始吧!

第一步:

猫会确定相对于开始位置(点A)的相邻方块,计算出他们的F值,然后把他们添加到open列表中:

注意每个方块中,F值(在左上角)是G(左下角)值和H(右下脚)值的和。

第二步:

在第二步中,猫选择了F值最小(为5)的方块,把它添加到closed列表中,然后检索它的相邻方块的相关数值,除了已经被添加到closed列表中的方块或者墙壁方块(因为它不能通行)以外,把相邻方块加入open列表。

注意被添加到open列表的两个新方块,相对于现在选中的方块来说,他们的G值都只是增加了1:

第三步:

再次,我们选择了相对于初始点A有最小F值(5)的方块,继续重复之前的步骤。现在,只有一个可能的方块被添加到open列表中了,因为已经有一个相邻的方块在close列表中,其他两个是墙壁方块:

现在我们遇到了一个有趣的情况。正如你看到的,相对于初始点A,有4个方块的F值都为7,我们要怎么做呢?!

第四步:

对于四个方块的F值都是7的情况,有几种解决方法可以使用,但是最简单(快速)的方法是一直跟着最近被添加到open列表中的方块。现在继续沿着最近被添加的方块前进:

这次有两个可通过的相邻方块了,我们还是像之前那样计算他们的和值F。

第五步:

接着我们选择了最小和值(7)的方块,继续重复之前的步骤:

我们差不多到终点了,但是这次你看到有两条到达骨头的最短路径提供给我们选择:

在我们的例子中,有两条最短路径:

1->2->3->4->5->6

1->2->3->4->5->7

选择哪一条其实没关系,然后,算法要做的所有事情就是返回,计算出最终的路径!

现在到了真正用代码实现的时候了。超级鸡冻的有木有~

4

算法实现

A-Star Algorithm

Python版:

import math

#地图
tm = [
'############################################################',
'#..........................................................#',
'#.............................#............................#',
'#.............................#............................#',
'#.............................#............................#',
'#.......S.....................#............................#',
'#.............................#............................#',
'#.............................#............................#',
'#.............................#............................#',
'#.............................#............................#',
'#.............................#............................#',
'#.............................#............................#',
'#.............................#............................#',
'#######.#######################################............#',
'#....#........#............................................#',
'#....#........#............................................#',
'#....##########............................................#',
'#..........................................................#',
'#..........................................................#',
'#..........................................................#',
'#..........................................................#',
'#..........................................................#',
'#...............................##############.............#',
'#...............................#........E...#.............#',
'#...............................#............#.............#',
'#...............................#............#.............#',
'#...............................#............#.............#',
'#...............................###########..#.............#',
'#..........................................................#',
'#..........................................................#',
'############################################################']

#因为python里string不能直接改变某一元素,所以用test_map来存储搜索时的地图
test_map = []

#########################################################

class Node_Elem:
   '''
   开放列表和关闭列表的元素类型,parent用来在成功的时候回溯路径
   '''
   def __init__(self, parent, x, y, dist):
       self.parent = parent
       self.x = x
       self.y = y
       self.dist = dist

class A_Star:
   '''
   A星算法实现类
   '''
   #注意w,h两个参数,如果你修改了地图,需要传入一个正确值或者修改这里的默认参数
   def __init__(self, s_x, s_y, e_x, e_y, w=60, h=30):
       self.s_x = s_x
       self.s_y = s_y
       self.e_x = e_x
       self.e_y = e_y

self.width = w
       self.height = h

self.open = []
       self.close = []
       self.path = []

#查找路径的入口函数
   def find_path(self):
       #构建开始节点
       p = Node_Elem(None, self.s_x, self.s_y, 0.0)
       while True:
           #扩展F值最小的节点
           self.extend_round(p)
           #如果开放列表为空,则不存在路径,返回
           if not self.open:
               return
           #获取F值最小的节点
           idx, p = self.get_best()
           #找到路径,生成路径,返回
           if self.is_target(p):
               self.make_path(p)
               return
           #把此节点压入关闭列表,并从开放列表里删除
           self.close.append(p)
           del self.open[idx]

def make_path(self,p):
       #从结束点回溯到开始点,开始点的parent == None
       while p:
           self.path.append((p.x, p.y))
           p = p.parent

def is_target(self, i):
       return i.x == self.e_x and i.y == self.e_y

def get_best(self):
       best = None
       bv = 1000000 #如果你修改的地图很大,可能需要修改这个值
       bi = -1
       for idx, i in enumerate(self.open):
           value = self.get_dist(i)#获取F值
           if value < bv:#比以前的更好,即F值更小
               best = i
               bv = value
               bi = idx
       return bi, best

def get_dist(self, i):
       # F = G + H
       # G 为已经走过的路径长度, H为估计还要走多远
       # 这个公式就是A*算法的精华了。
       return i.dist + math.sqrt(
           (self.e_x-i.x)*(self.e_x-i.x)
           + (self.e_y-i.y)*(self.e_y-i.y))*1.2

def extend_round(self, p):
       #可以从8个方向走
       xs = (-1, 0, 1, -1, 1, -1, 0, 1)
       ys = (-1,-1,-1,  0, 0,  1, 1, 1)
       #只能走上下左右四个方向
#        xs = (0, -1, 1, 0)
#        ys = (-1, 0, 0, 1)
       for x, y in zip(xs, ys):
           new_x, new_y = x + p.x, y + p.y
           #无效或者不可行走区域,则勿略
           if not self.is_valid_coord(new_x, new_y):
               continue
           #构造新的节点
           node = Node_Elem(p, new_x, new_y, p.dist+self.get_cost(
                       p.x, p.y, new_x, new_y))
           #新节点在关闭列表,则忽略
           if self.node_in_close(node):
               continue
           i = self.node_in_open(node)
           if i != -1:
               #新节点在开放列表
               if self.open[i].dist > node.dist:
                   #现在的路径到比以前到这个节点的路径更好~
                   #则使用现在的路径
                   self.open[i].parent = p
                   self.open[i].dist = node.dist
               continue
           self.open.append(node)

def get_cost(self, x1, y1, x2, y2):
       '''
       上下左右直走,代价为1.0,斜走,代价为1.4
       '''
       if x1 == x2 or y1 == y2:
           return 1.0
       return 1.4

def node_in_close(self, node):
       for i in self.close:
           if node.x == i.x and node.y == i.y:
               return True
       return False

def node_in_open(self, node):
       for i, n in enumerate(self.open):
           if node.x == n.x and node.y == n.y:
               return i
       return -1

def is_valid_coord(self, x, y):
       if x < 0 or x >= self.width or y < 0 or y >= self.height:
           return False
       return test_map[y][x] != '#'

def get_searched(self):
       l = []
       for i in self.open:
           l.append((i.x, i.y))
       for i in self.close:
           l.append((i.x, i.y))
       return l

#########################################################
def print_test_map():
   '''
   打印搜索后的地图
   '''
   for line in test_map:  
       print (''.join(line) )

def get_start_XY():  
   return get_symbol_XY('S')

def get_end_XY():  
   return get_symbol_XY('E')

def get_symbol_XY(s):  
   for y, line in enumerate(test_map):  
       try:  
           x = line.index(s)  
       except:  
           continue  
       else:  
           break  
   return x, y

#########################################################  
def mark_path(l):  
   mark_symbol(l, '*')

def mark_searched(l):  
   mark_symbol(l, ' ')

def mark_symbol(l, s):  
   for x, y in l:  
       test_map[y][x] = s

def mark_start_end(s_x, s_y, e_x, e_y):  
   test_map[s_y][s_x] = 'S'  
   test_map[e_y][e_x] = 'E'

def tm_to_test_map():  
   for line in tm:  
       test_map.append(list(line))

def find_path():  
   s_x, s_y = get_start_XY()  
   e_x, e_y = get_end_XY()  
   a_star = A_Star(s_x, s_y, e_x, e_y)  
   a_star.find_path()  
   searched = a_star.get_searched()  
   path = a_star.path  
   #标记已搜索区域  
   mark_searched(searched)  
   #标记路径  
   mark_path(path)  
   print ('path length is %d'%(len(path)) )
   print ('searched squares count is %d'%(len(searched))  )
   #标记开始、结束点  
   mark_start_end(s_x, s_y, e_x, e_y)

if __name__ == '__main__':  
   #把字符串转成列表  
   tm_to_test_map()  
   find_path()  
   print_test_map()  

运行后的结果为:

MATLAB版:

代码很长,树根就不放出来了。链接可以后台问树根拿,回复关键词:“matlab_A-Star” 即可

运行后会形成UI界面,并且起始点,目标点和障碍物的位置可自己调节:

感谢阅读

(0)

相关推荐