无心中在cocoaChina的首页看到了一篇介绍A*算法用swift实现的文章,对A*寻路算法产生了兴趣。在百度谷歌了不少文章后,终于A*算法的流程,同时让我发现了两篇很是好的英文文章:html
A* Pathfinding for Beginnersjava
第一篇文章是很是好的A*算法入门文章,通读一遍就基本能够用代码实现了;第二篇文章能够说给我带来了震撼,原来算法能够这样讲,推荐你们都看一下。算法
看完第二篇文章就产生了要学习做者的方式讲一下A*算法的冲动,同时也当是练练手,很久没写javaScript了。swift
讲解方式也按照<Introduction to A*>一文中的顺序,从最简单的广度优先算法(Breadth-First-Search)、大名鼎鼎的Dijkstra算法到Greed-Best-First-Search算法,最后是A*算法。app
(关于A*算法,网上资料不少,A*算法的变种也不少,有兴趣的朋友能够自行搜索,
本文仅对四种常见寻路算法进行简单介绍,如有不合理或错误之处,请谅解并在回复中指出)性能
广度优先算法是最简单的寻路算法,算法执行的结果是得到从地图上任意一点S到其余全部可达点的最短路径,这里只考虑上下左右四方向行走的状况,算法流程很是容易理解:学习
演示gif:测试
演示程序优化
蓝色方块是不可经过的,S为扫描的起始点,一层层向外扩展,最终全部可到达的节点都被扫描,这一过程有时被称为“flood fill”。对于每个被扫描的节点,为其添加一个指向父节点的方向箭头,而后你会发现,从地图上任意一点开始,只要沿着箭头的方向移动,总能走到起始点S,并且走过的路径必然是最短路径之一。
看到广度优先算法,最早想到的应用场景就是塔防,敌人老是从固定的一个或几个出生点出现,向着固定的一个或几个目标移动,咱们彻底能够在每一关开始前以出生点为起始点遍历整个地图,这样本关中怪物的移动路线就能够肯定了。
下面让咱们考虑如下场景,地图中存在森林、山岭和平原,角色在这些地形上移动时,移动力消耗是不一样的,好比《文明》中。这就要求咱们把每个区块的消耗考虑在内,这时,Dijkstra算法就能够发挥做用了。
在地图内的每一个区块移动消耗不一样时,Dijkstra算法能够很是方便的找出从地图上某个起始区块到其余全部可达区块的最短路径,这里仍然只考虑上下左右四个方向移动的状况,算法流程以下:
说明:起始区块记做S,从S到当前区块G的总移动消耗记做CG,优先队列openList中数据为(G,CG)(区块,S到当前区块总移动消耗),区块G自身移动消耗记做ZG。
演示gif :
蓝色区域不可经过,白色区块表明平原地形,移动一格消耗为1,绿色区块表明森林,移动一格消耗为5,黑色区块带表山脉,移动一格消耗为10。区块中的数字表示从起始点到当前区块的最小移动消耗。从演示程序能够看出,因为优先队列的存在,区块消耗越高,进入closeList的时间越靠后,这与广度优先算法中一层层向外扩展的方式不一样。
不难想到,当地图上的全部区块移动消耗相同时,Dijkstra算法就简化为广度优先算法,由于移动总消耗最低的区块总会是当前区块的相邻区块。
在《Introduction to A*》中,做者提出了一个很是有趣的Dijkstra算法的应用,在这里和你们分享下:在某个游戏中,当我但愿个人角色更倾向于通过某些区块时(好比通过这些区块能够得到增益效果、道具等等)或者倾向于躲避某些区块时(好比通过这些区块会丢失生命值,或者这些区块上的敌人很是危险),咱们能够经过调整这些区块的移动消耗来影响移动路径的产生从而影响角色的移动行为。一片区域上的移动消耗很小时,算法生成的最短路径会倾向于通过这片区域,如gif中要到达森林区块的右侧时,路径没有横穿森林,而是绕着森林边缘走的,反之亦然。
广度优先算法和Dijkstra算法都须要遍历整个地图,而在大多数场景中,咱们只须要知道一个点到另外一个点的最短路径,下面的Greed-Best-First-Search为咱们提供了一个思路。
网上没有找到比较官方的翻译,有人译做“最好优先贪婪算法”,咱们暂时这么称呼它。
最好优先贪婪算法与上面两种算法的不一样之处在于,它老是尝试向离目标节点更近的方向探索,怎样才算离目标节点更近呢?在只能上下左右四方向移动的前提下,咱们经过计算当前节点到目标节点的曼哈顿距离来进行判断。
假设当前节点坐标为(x,y),目标节点的坐标为(x1,y1),曼哈顿距离计算公式以下:
Manhattan_distance = abs(x1-x)+abs(y1-y)
因为曼哈顿距离只在两点之间没有障碍物的状况下才与实际距离相等,通常状况下曼哈顿距离老是小于实际距离。所以,当节点间不存在障碍物时,算法能够保证找出最短路径,可是一旦障碍物出现,最短路径就没法保证了。
算法流程以下:
说明:起始节点记做S,目标节点记做E,对于任意节点G,从当前节点G到目标节点E的曼哈顿距离记做MG,优先队列openList中数据为(G,MG)(节点,当前节点到目标节点E的曼哈顿距离)。
演示gif:
演示程序中,暗蓝色表示节点是障碍物,土黄色表示节点处于closeList中,淡蓝色表示节点处于openList中,白色表示节点处于搜索出的结果路径上。点击地图上的区块能够从新设置目标节点E。能够看出,当目标节点处于地图左下方时,搜索路径很明显不是最短路径。虽然算法不能保证能够找到最短路径,但当地形不复杂时(如gif中起点和终点间没有障碍物),路径搜索速度是四种算法中最快的。
最好优先贪婪算法虽然不能保证找出最短路径,但为咱们提供了一个思路,A*算法就是Dijkstra算法与最好优先贪婪算法结合后获得的算法。
A*算法与最好优先贪婪算法同样都经过计算一个值来判断探索的方向。对于节点N,计算公式以下:
F(N)=G(N)+H(N)
其中G(N)就是Dijkstra算法中计算的,从起点到当前节点N的移动消耗,而H(N),在只容许上下左右移动的前提下,就是最好优先贪婪算法中当前节点N到目标节点E的曼哈顿距离。所以,当节点间移动消耗很是小时,G对F的影响也会微乎其微,A*算法就退化为最好优先贪婪算法;当节点间移动消耗很是大以致于H对F的影响微乎其微时,A*算法就退化为Dijkstra算法。
算法流程以下:
说明:起始节点记做S,目标节点记做E,对于任意节点P,从S到当前节点P的总移动消耗记做GP,节点P到目标E的曼哈顿距离记做HP,从节点P到相邻节点N的移动消耗记做DPN,用于优先级排序的值F(N)记做FP。
演示gif:
演示gif中,土黄色表示节点在closeList中,淡蓝色表示节点在openList中,深蓝色表示节点不可经过,白色表示节点在搜索出的结果路径上。能够看出,A*算法老是设法保证搜索路径上的F值保持不变。
在继续测试演示程序时,我发现了一个问题:
因为我用JS开发的上述演示程序,没有趁手的优先队列,因此用Array客串了一把,每次取值时根据F值进行倒序排序,取队列末尾的值。从gif中能够看出,算法执行的并不完美,咱们固然但愿A*算法在简单环境下可以拥有和最好优先贪婪算法同样的运行速度,但个人A*算法却出现了无用的扫描,并不在搜索结果上的区域也参与了计算。怎样避免这一状况呢?
首先想到的固然是改进个人优先队列,但这有点麻烦啊,别着急,有更简单的方法,这个方法和接下来要了解的启发式算法有关。
关于最优选择贪婪算法和A*算法中的曼哈顿距离的运用属于启发式算法(Heuristic Algrathm)的一种,这也是A*算法公式F=G+H中H的由来。
这里摘抄一段《Introduction to A*》的做者在另外一篇文章《Heuristic》中的一小段,讲述H(n)如何影响A*算法的行为。
At one extreme, if h(n) is 0, then only g(n) plays a role, and A* turns into Dijkstra’s algorithm, which is guaranteed to find a shortest path.
If h(n) is always lower than (or equal to) the cost of moving from n to the goal, then A* is guaranteed to find a shortest path. The lower h(n) is, the more node A* expands, making it slower.
If h(n) is exactly equal to the cost of moving from n to the goal, then A* will only follow the best path and never expand anything else, making it very fast. Although you can’t make this happen in all cases, you can make it exact in some special cases. It’s nice to know that given perfect information, A* will behave perfectly.
If h(n) is sometimes greater than the cost of moving from n to the goal, then A* is not guaranteed to find a shortest path, but it can run faster.
At the other extreme, if h(n) is very high relative to g(n), then only h(n) plays a role, and A* turns into Greedy Best-First-Search.
翻译以下:
回到刚才的问题,这个简单的方法就是修改咱们的H(n),新的曼哈顿距离公式为
Manhattan_distance = abs(x1-x)+abs(y1-y)*1.01
咱们对上下方向的距离进行了轻微的调整,效果如何呢?
能够看到,扫描过程很是高效,全部closeList中的节点都出如今告终果路径上。这是由于A*算法会沿着F值变小的方向搜索,因为曼哈顿公式的调整,本来F值相等的节点再也不想等,同一列由上到下递减,这就产生了gif中的现象,结果路径老是先向下走,直到和目标节点同一行后,再向右走。
关于四种算法的选择
在状况容许的前提下,在生成地图或者加载地图时,记录地图上的特征区域。特征区域分为两类: