A*寻路算法

在看下面这篇文章之前,先介绍几个理论知识,有助于理解A*算法。

启发式搜索:启发式搜索就是在状态空间中的搜索对每一个搜索的位置进行评估,得到最好的位置,再从这个位置进行搜索直到目标。这样可以省略大量无谓的搜索路径,提到了效率。在启发式搜索中,对位置的估价是十分重要的。采用了不同的估价可以有不同的效果。

估价函数:从当前节点移动到目标节点的预估费用;这个估计就是启发式的。在寻路问题和迷宫问题中,我们通常用曼哈顿(manhattan)估价函数(下文有介绍)预估费用。

A*算法与BFS:可以这样说,BFS是A*算法的一个特例。对于一个BFS算法,从当前节点扩展出来的每一个节点(如果没有被访问过的话)都要放进队列进行进一步扩展。也就是说BFS的估计函数h永远等于0,没有一点启发式的信息,可以认为BFS是“最烂的”A*算法

选取最小估价:如果学过数据结构的话,应该可以知道,对于每次都要选取最小估价的节点,应该用到最小优先级队列(也叫最小二叉堆)。在C++的STL里有现成的数据结构priority_queue,可以直接使用。当然不要忘了重载自定义节点的比较操作符。

A*算法的特点:A*算法在理论上是时间最优的,但是也有缺点:它的空间增长是指数级别的

IDA*算法:这种算法被称为迭代加深A*算法,可以有效的解决A*空间增长带来的问题,甚至可以不用到优先级队列。

 

(1)搜索区域:

假设有人想从A点移动到一墙之隔的B点,如下图,绿色的是起点A,红色是终点B,蓝色方块是中间的墙:

  如图所示简易地图, 其中绿色方块的是起点 (用 A 表示), 中间蓝色的是障碍物, 红色的方块 (用 B 表示) 是目的地. 为了可以用一个二维数组来表示地图, 我们将地图划分成一个个的小方块.

  二维数组在游戏中的应用是很多的, 比如贪吃蛇和俄罗斯方块基本原理就是移动方块而已. 而大型游戏的地图, 则是将各种"地貌"铺在这样的小方块上.

(2)开始搜索

  1,从点A开始,并且把它作为待处理点存入一个“开启列表”。开启列表就像一张购物清单。尽管现在列表里只有一个元素,但以后就会多起来。你的路径可能会通过它包含的方格,也可能不会。基本上,这是一个待检查方格的列表。

  2,寻找起点周围所有可到达或者可通过的方格,跳过有墙,水,或其他无法通过地形的方格。也把他们加入开启列表。为所有这些方格保存点A作为“父方格”。当我们想描述路径的时候,父方格的资料是十分重要的。后面会解释它的具体用途。

  3,从开启列表中删除点A,把它加入到一个“关闭列表”, 列表中保存所有不需要再次检查的方格。在这一点,你应该形成如图的结构。在图中,暗绿色方格是你起始方格的中心。它被用浅蓝色描边,以表示它被加入到关闭 列表中了。所有的相邻格现在都在开启列表中,它们被用浅绿色描边。每个方格都有一个灰色指针反指他们的父方格,也就是开始的方格。

(3)路径评分

  从 "开启列表" 中找出相对最靠谱的方块, 什么是最靠谱? 它们通过公式 F=G+H 来计算.

F = G + H
    # G 表示从起点 A 移动到网格上指定方格的移动耗费 (可沿斜方向移动).
    # H 表示从指定的方格移动到终点 B 的预计耗费 (H 有很多计算方法, 这里我们设定只可以上下左右移动,注意,这个的上下左右移动仅仅只是在计算H时使用,当路径移动的时候,是可以有8个方向的).

  我们假设横向移动一个格子的耗费为10, 为了便于计算, 沿斜方向移动一个格子耗费是14. 为了更直观的展示如何运算 FGH, 图中方块的左上角数字表示 F, 左下角表示 G, 右下角表示 H.如下图:

(4)继续搜索

从 "开启列表" 中选择 F 值最低的方格 C (绿色起始方块 A 右边的方块), 然后对它进行如下处理(参照下图):

  4. 把它从 "开启列表" 中删除, 并放到 "关闭列表" 中.

  5. 检查它所有相邻并且可以到达 (障碍物和 "关闭列表" 的方格都不考虑,本例中,也不考虑穿越拐角的情况) 的方格. 如果这些方格还不在 "开启列表" 里的话, 将它们加入 "开启列表", 计算这些方格的 G, H 和 F 值各是多少, 并设置它们的 "父方格" 为 C.

  6. 如果某个相邻方格 D 已经在 "开启列表" 里了, 检查如果用新的路径 (就是经过C 的路径) 到达它的话, G值是否会更低一些, 如果新的G值更低, 那就把它的 "父方格" 改为目前选中的方格 C, 然后重新计算它的 F 值和 G 值 (H 值不需要重新计算, 因为对于每个方块, H 值是不变的). 如果新的 G 值比较高, 就说明经过 C 再到达 D 不是一个明智的选择, 因为它需要更远的路, 这时我们什么也不做.

  7. 如果某个相邻方格D已经在"关闭列表"里了,检查如果用新的路径 (就是经过C 的路径) 到达它的话, G值是否会更低一些,如果新的G值更低,那就把它的 "父方格" 改为目前选中的方格 C, 然后重新计算它的 F 值和 G 值 (H 值不需要重新计算, 因为对于每个方块, H 值是不变的), 同时重新打开该节点并将其从"关闭列表"删除(即重新放入OPEN集).

  如上图, 我们选中了 C 因为它的 F 值最小, 我们把它从 "开启列表" 中删除, 并把它加入 "关闭列表". 它右边上下三个都是墙, 所以不考虑它们. 它左边是起始方块, 已经加入到 "关闭列表" 了, 也不考虑. 所以它周围的候选方块就只剩下 4 个. 让我们来看看 C 下面的那个格子, 它目前的 G 是14, 如果通过 C 到达它的话, G将会是 10 + 10, 这比 14 要大, 因此我们什么也不做.

  然后我们继续从 "开启列表" 中找出 F 值最小的, 但我们发现 C 上面的和下面的同时为 54, 这时怎么办呢? 这时随便取哪一个都行, 比如我们选择了 C 下面的那个方块 D.如下图:

  D右边已经右上方的都是墙, 所以不考虑, 但为什么右下角的没有被加进 "开启列表" 呢? 因为你不能在不穿越墙角的情况下直接到达那个格子。你的确需要先往下走然后到达那一格,按部就班的走过那个拐角。(注解:穿越拐角的规则是可选的,它取决于你的节点是如何放置的. 我们图中的示例不允许这样走).

  就这样, 我们从 "开启列表" 找出 F 值最小的, 将它从 "开启列表" 中移掉, 添加到 "关闭列表". 再继续找出它周围可以到达的方块, 如此循环下去...

  那么什么时候停止呢? —— 当我们发现 "开始列表" 里出现了目标终点方块的时候, 说明路径已经被找到.如下图:

(5)返回路径

  搜索完成. 现在,我们怎么确定这条路径呢?很简单,从红色的目标格开始,按箭头的方向朝父节点移动。这最终会引导你回到起始格,这就是你的路径!看起来应该像图中那样。从起始格A移动到目标格B只是简单的从每个格子(节点)的中点沿路径移动到下一个,直到你到达目标点。就这么简单。如下图:

  注意:起始格下方第2个格子(D格子的左下角格子)的父节点已经和前面不同的。之前它的G值是88,并且指向右上方的格子(D格子)。现在它的G值是80,指向它上方的格子(D格子左边的格子)。这在寻路过程中的某处发生,当应用新路径时,G值经过检查变得低了, 于是父节点被重新指定,GF值被重新计算。

(6)整个搜索过程

  1,把起始格添加到开启列表。

  2,重复如下的工作:

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

    b) 把它切换到关闭列表。

    c) 对相邻的格中的每一个?

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

      * 如果它不在开启列表中,把它添加进去。把当前格作为这一格的父节点。记录这一格的F,G,H值。

      * 如果它已经在开启列表中,用G值为参考检查新的路径是否更好。更低的G值意味着更好的路径。如果是这样,就把这一格的父节点改成当前格,并且重新计算这一格的GF值。如果你保持你的开启列表按F值排序,改变之后你可能需要重新对开启列表排序。

      * 如果它已经在关闭列表中,用G值为参考检查新的路径是否更好。如果是,就把这一格的父节点改成当前格,重新计算这一格的G和F值。同时将它从关闭列表删除,并重新加入开启列表.

    d) 停止,当你

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

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

  3.保存路径。从目标格开始,沿着每一格的父节点移动直到回到起始格。这就是你的路径。

 

-------------------------------------------------------------------------------------------------

  A*算法最为核心的过程,就在每次选择下一个当前搜索点时,是从所有已探知的但未搜索过点中(可能是不同层,亦可不在同一条支路上),选取f值最小的结点进行展开. 而所有“已探知的但未搜索过点”可以通过一个按f值升序的队列(即优先队列)进行排列。这样,在整体的搜索过程中,只要按照类似广度优先的算法框架,从优先队列中弹出队首元素(f值),对其可能子结点计算g、h和f值,直到优先队列为空(无解)或找到终止点为止。
  A*算法与广度、深度优先和Dijkstra 算法的联系就在于:当 g(n)=0时,该算法类似于DFS,当h(n)=0时,该算法类似于BFS。且同时,如果h(n)为0,只需求出g(n),即求出起点到任意顶点n的最 短路径,则转化为单源最短路径问题,即Dijkstra算法。这一点,可以通过上面的A*搜索树的具体过程中将h(n)设为0或将g(n)设为0而得到。

-------------------------------------------------------------------------------------------------

附录,简单实现代码:

https://github.com/yangxt225/AStar

posted @ 2016-04-30 13:11  小天_y  阅读(970)  评论(0编辑  收藏  举报