算法修养--A*寻路算法

算法,修养,寻路 · 浏览次数 : 11

小编点评

**A*算法核心** **主要参数** * startNode:开始节点 * targetNode:目标节点 **返回值** * 最短路径 **异常** * Exception:没有可访问的节点 **算法步骤** 1. 初始化到搜索的节点列表 2. 循环搜索到搜索的节点列表 3. 对于每个搜索到的节点,更新其优先级 4. 找出最优先级的节点作为开始节点 5. 从开始节点到目标节点找到路径 6. 返回路径 **优化** * 使用堆排序优化搜索的效率 * 使用启发函数来减少搜索的节点数量 * 缓存搜索结果以减少再次搜索 **注意事项** * A*算法只对于可访问的节点搜索路径 * 算法的效率随着搜索的节点数量增加而下降 * 算法对障碍物的处理效率可能下降

正文

A*寻路算法

广度优先算法

广度优先算法搜索以广度做为优先级进行搜索。

从起点开始,首先遍历起点周围邻近的点,然后再遍历已经遍历过的点邻近的点,逐步的向外扩散,直到找到终点。

这种算法就像洪水(Flood fill)一样向外扩张。直至洪水将整张地图都漫延。在访问节点时候,每个点需要记录到达该点的前一个点的位置(父节点),访问到终点时候,便可以从终点沿着父节点一路走回到起点,从而找出路径。(注意:这也是A*算法的一部分)

这种洪水蔓延式寻找路径的方式较为野蛮粗暴,仅仅依据广度来找路径难以找到最短的路径

image

Dijkstra算法

在实际寻路场景中,要考虑“移动成本”。不同的路径有不同的成本,例如,穿过平原或沙漠可能需要 1 个移动点,但穿过森林或丘陵可能需要 5 个移动点。玩家在水中行走的成本是在草地上行走的 10 倍。为此我们需要Dijkstra算法。

在Dijkstra算法中,需要计算每一个节点距离起点移动的总移动代价,同时,还需要一个优先队列结构,对于所有待遍历的节点,放入优先队列中,优先队列会按照代价进行排序。(这也是在下面要介绍的A*算法实现案例中待优化的点!)

在算法运行过程中,每次都从优先队列中选出移动成本最小的作为下一个要遍历检查的节点,直到访问到达终点为止。

image

两种算法的对比:

考虑这样一种场景,在一些情况下,图形中相邻节点之间的移动代价并不相等。例如,游戏中的一幅图,既有平地也有山脉,那么游戏中的角色在平地和山脉中移动的速度通常是不相等的。

在Dijkstra算法中,需要计算每一个节点距离起点的总移动代价。同时,还需要一个优先队列结构。对于所有待遍历的节点,放入优先队列中会按照代价进行排序。

在算法运行的过程中,每次都从优先队列中选出代价最小的作为下一个遍历的节点。直到到达终点为止。

下面对比了不考虑节点移动代价差异的广度优先搜索与考虑移动代价的Dijkstra算法的运算结果:

广度优先算法: Dijkstra算法:

image

当然,如果不考虑移动代价的因素,在同一个网格图中Dijkstra算法将和广度优先算法一样。

启发式搜索?

通过广度优先搜索和 Dijkstra 算法,边界向各个方向扩展。如果试图找到通往所有位置或许多位置的路径,这是一个合理的选择。

如果仅仅是找到到达一个位置的路径,我们应当控制洪水的流向,时期朝向目标位置流动。而控制方向的条件判断便可以通过启发式搜索来完成。

而所谓启发式搜索函数便是用来计算当前点到达目标点的距离(步数),预测到达目标点的距离。

启发函数来计算到目标点距离方式有多种选择:

  • 曼哈顿距离

    如果图形中只允许朝上下左右四个方向移动,则启发函数可以使用曼哈顿距离,它的计算方法如下图所示:

    img

    曼哈顿距离=abs(node.x-target.x)+abs(node.y-target.y)

  • 对角移动

    如果图形中允许斜着朝邻近的节点移动,则启发函数可以使用对角距离。

    img

  • 欧几里得距离

    如果图形中允许朝任意方向移动,则可以使用欧几里得距离。

    欧几里得距离2=(node.x-target.x)2+(node.y-target.y)2

采用启发函数控制广度优先搜索的方式为(贪婪)最佳优先搜索。

最佳优先搜索(Best First)

最佳优先搜索的原理也简单,与Dijkstra算法类似(广度优先+移动成本),也使用一个优先队列存储启发函数值(该点与目标点的距离),每次选取与目标点最近的点(邻居)来访问,直至访问到终点。

可以理解算法是广度优先算法+选择最小目标点距离参数的结果。

image

同样,由于没有考虑移动成本,在移动成本不同的地图中也不能考虑到森林、沼泽等移动速度不同的场景造成的移动成本的增加。不能保证找到的路径是最短的路径。

那就结合在一起呗!

A*算法=广度优先算法+考虑“移动成本”+考虑“与目标点的距离”;

既可以保证聪明地选择好走的路线避开难走的路线或者障碍物,也可以集中管控搜索方向向着目标点中的位置搜寻。

下面是对三个算法的详细对比:

A*算法集合了前两个算法的优势,可以保证在两点之间找到最佳的路线。

image

A*寻路算法

基本原理:

A*算法使用如下所示的函数来计算每个节点的优先级:

\[f(n)=g(n)+h(n) \]

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);

image

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

image

代码实现:

//节点类
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*寻路算法相似的内容:

算法修养--A*寻路算法

本文从广度优先算法为切入点,介绍了广度优先算法、Dijkstra算法、最佳优先搜索以及A*寻路算法, 并展示核心算法代码实现。

拆解雪花算法生成规则

雪花算法(Snowflake)是一种生成分布式全局唯一ID的算法,生成的ID称为Snowflake IDs或snowflakes。这种算法由Twitter创建,并用于推文的ID。目前仓储平台生成ID是用的雪花算法修改后的版本。

关于去除图片上的水印

现在有很多去除水印的工具,但基本上都需要你花钱。作为资深白嫖党,想让我花钱,那是不可能的。 于是我做了下research(search, search, research…),我发现现在的“去水印”基本上都是一个思路:利用图像修复算法。把有水印的地方看作是图像损坏的地方,用相邻像素替换那些损坏的地方

8.1 C++ STL 变易拷贝算法

C++ STL中的变易算法(Modifying Algorithms)是指那些能够修改容器内容的算法,主要用于修改容器中的数据,例如插入、删除、替换等操作。这些算法同样定义在头文件 algorithm中,它们允许在容器之间进行元素的复制、拷贝、移动等操作,从而可以方便地对容器进行修改和重组。

算法金 | 通透!!十大回归算法模型最强总结

大侠幸会,在下全网同名「算法金」 0 基础转 AI 上岸,多个算法赛 Top 「日更万日,让更多人享受智能乐趣」 今日 170+/10000 问:算法那么多,怎么修炼的过来 答:搞定最经典的,这些是低垂的果实 前几天发出吴恩达:机器学习的六个核心算法! 这篇文章,读者反馈很好,特别推荐阅读。 吴恩达

7.1 C++ STL 非变易查找算法

C++ STL 中的非变易算法(Non-modifying Algorithms)是指那些不会修改容器内容的算法,是C++提供的一组模板函数,该系列函数不会修改原序列中的数据,而是对数据进行处理、查找、计算等操作,并通过迭代器实现了对序列元素的遍历与访问。由于迭代器与算法是解耦的,因此非变易算法可以广泛地应用于各种容器上,提供了极高的通用性和灵活性。

算法学习笔记(3.1): ST算法

ST表 在RMQ(区间最值)问题中,著名的ST算法就是倍增的产物。ST算法可以在 \(O(n \log n)\) 的时间复杂度能预处理后,以 \(O(1)\) 的复杂度在线回答区间 [l, r] 内的最值。 当然,ST表不支持动态修改,如果需要动态修改,线段树是一种良好的解决方案,是 \(O(n)\

初探富文本之OT协同算法

初探富文本之OT协同算法 OT的英文全称是Operational Transformation,是一种处理协同编辑的算法。当前OT算法用的比较多的地方就是富文本编辑器领域了,常用于作为实现文档协同的底层算法,支持多个用户同时编辑文档,不会因为用户并发修改导致冲突,而导致结果不一致甚至数据丢失的问题。

软件设计模式系列之二十三——策略模式

策略模式(Strategy Pattern)是一种行为型设计模式,它允许在运行时动态选择算法的行为。这意味着你可以定义一系列算法,将它们封装成独立的策略对象,然后根据需要在不修改客户端代码的情况下切换这些算法。策略模式有助于解决问题领域中不同行为的变化和扩展,同时保持代码的灵活性和可维护性。

opengrok源代码在线阅读平台搭建及字体修改

服务搭建 我所编写的docker-compose.yml如下,成功运行后将源码目录移动至 /data/opengrok/src ,重启容器使得opengrok快速更新索引 services: opengrok: container_name: opengrok # 1.6版本在使用中还算稳定 ima