算法修养--A*寻路算法
A*寻路算法
广度优先算法
广度优先算法搜索以广度做未优先级进行搜索。
从起点开始,首先遍历起点周围邻近的点,然后再遍历已经遍历过的点邻近的点,逐步的向外扩散,直到找到终点。
这种算法就像洪水(Flood fill)一样向外扩张。直至洪水将整张地图都漫延。在访问节点时候,每个点需要记录到达该点的前一个点的位置(父节点),访问到终点时候,便可以从终点沿着父节点一路走回到起点,从而找出路径。(注意:这也是A*算法的一部分)
这种洪水蔓延式寻找路径的方式较为野蛮粗暴,仅仅依据广度来找路径,难以找到最短的路径。

Dijkstra算法
在实际寻路场景中,要考虑“移动成本”。不同的路径有不同的成本,例如,穿过平原或沙漠可能需要 1 个移动点,但穿过森林或丘陵可能需要 5 个移动点。玩家在水中行走的成本是在草地上行走的 10 倍。为此我们需要Dijkstra算法。
在Dijkstra算法中,需要计算每一个节点距离起点移动的总移动代价,同时,还需要一个优先队列结构,对于所有待遍历的节点,放入优先队列中,优先队列会按照代价进行排序。(这也是在下面要介绍的A*算法实现案例中待优化的点!)
在算法运行过程中,每次都从优先队列中选出移动成本最小的作为下一个要遍历检查的节点,直到访问到达终点为止。

两种算法的对比:
考虑这样一种场景,在一些情况下,图形中相邻节点之间的移动代价并不相等。例如,游戏中的一幅图,既有平地也有山脉,那么游戏中的角色在平地和山脉中移动的速度通常是不相等的。
在Dijkstra算法中,需要计算每一个节点距离起点的总移动代价。同时,还需要一个优先队列结构。对于所有待遍历的节点,放入优先队列中会按照代价进行排序。
在算法运行的过程中,每次都从优先队列中选出代价最小的作为下一个遍历的节点。直到到达终点为止。
下面对比了不考虑节点移动代价差异的广度优先搜索与考虑移动代价的Dijkstra算法的运算结果:
广度优先算法: Dijkstra算法:

当然,如果不考虑移动代价的因素,在同一个网格图中Dijkstra算法将和广度优先算法一样。
启发式搜索?
通过广度优先搜索和 Dijkstra 算法,边界向各个方向扩展。如果试图找到通往所有位置或许多位置的路径,这是一个合理的选择。
如果仅仅是找到到达一个位置的路径,我们应当控制洪水的流向,时期朝向目标位置流动。而控制方向的条件判断便可以通过启发式搜索来完成。
而所谓启发式搜索函数便是用来计算当前点到达目标点的距离(步数),预测到达目标点的距离。
启发函数来计算到目标点距离方式有多种选择:
曼哈顿距离
如果图形中只允许朝上下左右四个方向移动,则启发函数可以使用曼哈顿距离,它的计算方法如下图所示:

曼哈顿距离=abs(node.x-target.x)+abs(node.y-target.y)
对角移动
如果图形中允许斜着朝邻近的节点移动,则启发函数可以使用对角距离。

欧几里得距离
如果图形中允许朝任意方向移动,则可以使用欧几里得距离。
欧几里得距离2=(node.x-target.x)2+(node.y-target.y)2
采用启发函数控制广度优先搜索的方式为(贪婪)最佳优先搜索。
最佳优先搜索(Best First)
最佳优先搜索的原理也简单,与Dijkstra算法类似(广度优先+移动成本),也使用一个优先队列存储启发函数值(该点与目标点的距离),每次选取与目标点最近的点(邻居)来访问,直至访问到终点。
可以理解算法是广度优先算法+选择最小目标点距离参数的结果。

同样,由于没有考虑移动成本,在移动成本不同的地图中也不能考虑到森林、沼泽等移动速度不同的场景造成的移动成本的增加。不能保证找到的路径是最短的路径。
那就结合在一起呗!
A*算法=广度优先算法+考虑“移动成本”+考虑“与目标点的距离”;
既可以保证聪明地选择好走的路线避开难走的路线或者障碍物,也可以集中管控搜索方向向着目标点中的位置搜寻。
下面是对三个算法的详细对比:
A*算法集合了前两个算法的优势,可以保证在两点之间找到最佳的路线。

A*寻路算法
基本原理:
A*算法使用如下所示的函数来计算每个节点的优先级:
\]
f(n)是节点n的综合优先级。
g(n)表示节点n距离起点的代价(距离或步数),即:从起点出发到达当前点所走的步数;
h(n)则表示节点n距离终点(目标点)的预估代价,即:从当前点达到终点需要走的步数,这是A*算法的启发函数,相当于提前预测要走到目标点还需要的步数。由于是预测,按照最短的路径(步数)来表示,所以实际上到达终点的路程要等于或大于预测的数值。
寻路算法的启发函数控制:(理解两个参数对搜寻的影响)
- 在极端情况下,当启发函数h(n)始终为0,则将由g(n)决定节点的优先级,此时算法就退化成了Dijkstra算法。
- 如果h(n)始终小于等于节点n到终点的代价,则A*算法保证一定能够找到最短路径。但是当h(n)的值越小,算法将遍历越多的节点,也就导致算法越慢。
- 如果h(n)完全等于节点n到终点的代价,则A*算法将找到最佳路径,并且速度很快。可惜的是,并非所有场景下都能做到这一点。因为在没有达到终点之前,我们很难确切算出距离终点还有多远。
- 如果h(n)的值比节点n到终点的代价要大,则A*算法不能保证找到最短路径,不过此时会很快。
- 在另外一个极端情况下,如果h()n相较于g(n)大很多,则此时只有h(n)产生效果,这也就变成了最佳优先搜索。
在寻路过程中,每次依据周边节点的f(n)数值来选择,每次选择最小的f(n);

有障碍物的情况下的寻路(黑色方块为障碍物)

代码实现:
//节点类
public class NodeBase{
public NodeBase Connection{get;private set;}//存储自己的相连接路径中的节点
public float G{get;private set;}
public float H{get;private set;}
public float F=>G+H;
public void SetConnetion(NodeBase nodeBase)=>Connection=nodeBase;
public void SetG(float g)=>G=g;
public void SetH(float h)=>H=h;
}
//核心算法
public static class Pathfinding {
/// <summary>
/// A*算法核心
/// </summary>
/// <param name="startNode">开始点</param>
/// <param name="targetNode">目标点</param>
/// <returns>最短路径</returns>
/// <exception cref="Exception"></exception>
public static List<NodeBase> FindPath(NodeBase startNode, NodeBase targetNode) {
var toSearch = new List<NodeBase>() { startNode };//要处理搜寻的节点(未访问过)
var processed = new List<NodeBase>();//存储访问过的节点
while (toSearch.Any()) {//判断是否有要搜寻的元素节点
//fixit 待优化的点 可以使用一个堆排序思想
var current = toSearch[0];
foreach (var t in toSearch)
if (t.F < current.F || t.F == current.F && t.H < current.H) current = t;
processed.Add(current);
toSearch.Remove(current);//搜寻过后就移除
current.SetColor(ClosedColor);
//终点检查
if (current == targetNode) {
var currentPathTile = targetNode;
var path = new List<NodeBase>();//保存路径
var count = 100;
//从终点到出发点的节点路径寻找
while (currentPathTile != startNode) {
path.Add(currentPathTile);
currentPathTile = currentPathTile.Connection;
count--;//这里根据地图的大小设置一个路径长度极限(可以忽略)
if (count < 0) throw new Exception();
Debug.Log("sdfsdf");
}
foreach (var tile in path) tile.SetColor(PathColor);
startNode.SetColor(PathColor);
Debug.Log(path.Count);
return path;//返回路径
}
//关心一下自己的邻居--可以到达的邻居,并且还未访问过的(非障碍物)
foreach (var neighbor in current.Neighbors.Where(t => t.Walkable && !processed.Contains(t))) {
var inSearch = toSearch.Contains(neighbor);//获取对邻居的访问状况
var costToNeighbor = current.G + current.GetDistance(neighbor);
//更新一下邻居的G值
//对于已经访问过的邻居 如果从另一个方向过来想再次访问,那么G值应当比之前访问的要小(只能是最近路程访问)
if (!inSearch || costToNeighbor < neighbor.G) {
neighbor.SetG(costToNeighbor);
neighbor.SetConnection(current);//记录与之连接的节点(算作路径中)
//如果是第一次访问 还需要进行H值的计算
if (!inSearch) {
neighbor.SetH(neighbor.GetDistance(targetNode));//计算启发函数值
toSearch.Add(neighbor);//将邻居添加到要访问的列表中
}
}
}
}
return null;
}
}
参考文章:https://www.redblobgames.com/pathfinding/a-star/introduction.html
参考视频:https://www.youtube.com/watch?v=i0x5fj4PqP4
参考项目:https://github.com/zhm-real/PathPlanning
算法修养--A*寻路算法的更多相关文章
- 算法:Astar寻路算法改进,双向A*寻路算法
早前写了一篇关于A*算法的文章:<算法:Astar寻路算法改进> 最近在写个js的UI框架,顺便实现了一个js版本的A*算法,与之前不同的是,该A*算法是个双向A*. 双向A*有什么好处呢 ...
- 算法:Astar寻路算法改进
早前写了一篇<RCP:gef智能寻路算法(A star)> 出现了一点问题. 在AStar算法中,默认寻路起点和终点都是N x N的方格,但如果用在路由上,就会出现问题. 如果,需要连线的 ...
- 一种高效的寻路算法 - B*寻路算法
在此把这个算法称作B* 寻路算法(Branch Star 分支寻路算法,且与A*对应),本算法适用于游戏中怪物的自动寻路,其效率远远超过A*算法,经过测试,效率是普通A*算法的几十上百倍. 通过引入该 ...
- 数据结构和算法总结(三):A* 寻路算法
前言 复习下寻路相关的东西,而且A star寻路在游戏开发中应用挺多的,故记录下. 正文 迪杰斯特拉算法 说起A*得先谈谈Dijkstra算法,它是在BFS基础上的一种带权值的两点最短寻路贪心算法. ...
- 基于Unity的A星寻路算法(绝对简单完整版本)
前言 在上一篇文章,介绍了网格地图的实现方式,基于该文章,我们来实现一个A星寻路的算法,最终实现的效果为: 项目源码已上传Github:AStarNavigate 在阅读本篇文章,如果你对于里面提到的 ...
- A星寻路算法介绍
你是否在做一款游戏的时候想创造一些怪兽或者游戏主角,让它们移动到特定的位置,避开墙壁和障碍物呢? 如果是的话,请看这篇教程,我们会展示如何使用A星寻路算法来实现它! 在网上已经有很多篇关于A星寻路算法 ...
- A*寻路算法探究
A*寻路算法探究 A*算法常用在游戏的寻路,是一种静态网路中求解最短路径的搜索方法,也是解决很多搜索问题的算法.相对于Dijkstra,BFS这些算法在复杂的搜索更有效率.本文在U3D中进行代码的测试 ...
- A*寻路算法
对于初学者而言,A*寻路已经是个比较复杂的算法了,为了便于理解,本文降低了A*算法的难度,规定只能横竖(四方向)寻路,而无法直接走对角线,使得整个算法更好理解. 简而言之,A*寻路就是计算从起点经过该 ...
- js实现A*寻路算法
这两天在做百度前端技术学院的题目,其中有涉及到寻路相关的,于是就找来相关博客进行阅读. 看了Create Chen写的理解A*寻路算法具体过程之后,我很快就理解A*算法的原理.不得不说作者写的很好,通 ...
- 用简单直白的方式讲解A星寻路算法原理
很多游戏特别是rts,rpg类游戏,都需要用到寻路.寻路算法有深度优先搜索(DFS),广度优先搜索(BFS),A星算法等,而A星算法是一种具备启发性策略的算法,效率是几种算法中最高的,因此也成为游戏中 ...
随机推荐
- Java有一对兔子,从出生后第3个月起每个月都生一对兔子,小兔子长到第三个月后每个月又生一对兔子,假如兔子都不死,问每个月的兔子总数为多少?
代码如下: public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out. ...
- MODBUS-TCP转Ethernet IP 网关连接空压机 配置案例
本案例是工业现场应用捷米特JM-EIP-TCP的Ethernet/IP转Modbus-TCP网关连接欧姆龙PLC与空压机的配置案例.使用设备:欧姆龙PLC,捷米特JM-EIP-TCP网关, ETH ...
- P7561[JOISC 2021 Day2] 道路の建設案 (Road Construction) 题解
P7561[JOISC 2021 Day2] 道路の建設案 (Road Construction) 题解 题目描述 JOI 国是一个 \(x\times y\) 的二维平面,王国里有 \(n\) 个城 ...
- .Net Core 如何数据导出 Excel?(EPPlus->OfficeOpenXml 实现固定列和动态列导出)
〇.前言 对于将数据以 Excel 表格文件输出,还是比较常用的,也存在诸多情况,比如列固定或不固定.数据类型为 List<T>或 Json 对象等. 本文通过包 OfficeOpenXm ...
- VSCode设置第三方字体
最近需要写C,所以下了一个VSCode IDE嘛 当然是美观最重要了(不是 个人比较喜欢JetBrains家的字体 在IDEA中主要使用的是 JetBrains Mono 想着把VSCode的字体也设 ...
- 【WebRtc】获取媒体设备信息
加载设备信息页面 加载完设备信息页面 Code /** * 加载当前设备的音视频信息 */ initInnerLocalDevice() { let that = this; // 判断是否支持 if ...
- 伸展树(Splay)详解
引入 在一条链中,二叉查找树的时间复杂度就会退化成 \(O(n)\),这时我们就需要平衡树来解决这个问题. \(Splay\)(伸展树)是平衡树的一种,它的每一步插入.查找和删除的平摊时间都是 \(O ...
- PerfView 洞察C#托管堆内存 "黑洞现象"
一:背景 1. 讲故事 首先声明的是这个 黑洞 是我定义的术语,它是用来表示 内存吞噬 的一种现象,何为 内存吞噬,我们来看一张图. 从上面的 卦象图 来看,GCHeap 的 Allocated=85 ...
- linux 字符集与编码格式相关
字符集:多个字符的集合. # 书写系统字母与符号的集合. 字符编码:把 字符集 中的字符 编码为(映射)指定集合中的某一对象. # 以便文本在计算机中存储和通过通信网络的传递 查看文件的的编码格式 ...
- jQuery事件自动触发
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...