A星算法说明

article/2025/10/2 21:42:48
A*算法说明

文章目录

前言

  因为最近要写一个毕业设计,有用到自动寻路的功能,因为我要在一个机器里跑算法然后控制机器人自动按照路线到达目的地,所以用Python等解释型语言或Unity等游戏引擎写这个算法都不太合适,我使用的机器要尽可能不在里面安装大型的库。所以我就用C++实现了一个A*算法。因为实现了之后觉得这个算法比较有意思,就又写了一个GUI程序,可以选择显示过程,即以可视化查看算法寻路的过程。
  我写的A*算法在能找到最优路线的前提下,支持斜方位移动(可以选择是否允许斜方位移动),支持设置道路拥堵情况(默认所有位置路况为1,如果设置大于1,则表示拥堵,数值越大则越拥堵,如果设置小于1,则表示比默认路况更为畅通,数值越小则越通畅,如果设置为0表示异常畅通,即通过此道路代价为0,如果设置为负数表示 + ∞ +\infty +,即无法通行),支持选择是否使用优先队列,支持读取和保存地图,在GUI程序里支持显示寻找路线的动画。

原理说明

  A*可以认为是添加了启发式函数的Dijkstra算法,在Dijkstra算法的基础上,构造一个函数 h ( n ) h(n) h(n),n为当前扩展结点, h ( n ) h(n) h(n)返回结点n到终点的开销估计。然后建立函数 f ( n ) = g ( n ) + h ( n ) f(n)=g(n)+h(n) f(n)=g(n)+h(n),其中 g ( n ) g(n) g(n)为从起点到结点n已经使用了的代价,所以 f ( n ) f(n) f(n)可以理解为是“从起点出发经过结点n再到终点的代价估计”
  关于 h ( n ) h(n) h(n)的构造返回值的问题会对A*算法造成影响:

  如果构造 h ( n ) ≡ 0 h(n) \equiv 0 h(n)0,那么该A*算法就已经退化为了Dijkstra算法,一定能解得最优解但是运行效率最低。
  如果构造 h ( n ) ≡ h ∗ ( n ) h(n) \equiv h^*(n) h(n)h(n),则该A*算法不仅能够保证一定能解得最优解,而且运行效率在所有能保证解得最优解的A*算法中是最高的。其中 h ∗ ( n ) h^*(n) h(n)表示结点n到终点的实际代价。
  如果构造的 h ( n ) h(n) h(n)对所有的n有 h ( n ) ≤ h ∗ ( n ) h(n) \leq h^*(n) h(n)h(n),则该A*算法能保证一定能得到最优解,但是效率略低于上述 h ( n ) ≡ h ∗ ( n ) h(n) \equiv h^*(n) h(n)h(n)的情况。
  如果构造的 h ( n ) h(n) h(n)存在n使得 h ( n ) > h ∗ ( n ) h(n)>h^*(n) h(n)>h(n),则该A*算法不一定能得到最优解(当然运气好的时候也有可能会解得最优解,但是不能保证)。

  所以,我们应该构造满足对所有的n有 h ( n ) ≤ h ∗ ( n ) h(n) \leq h^*(n) h(n)h(n) h ( n ) h(n) h(n)。虽然 h ( n ) ≡ h ∗ ( n ) h(n) \equiv h^*(n) h(n)h(n)效果最好,但是面对复杂的地图,这种启发式函数可欲而不可求(当然, h ( n ) h(n) h(n)函数必须要是复杂度极低的,不能说我为了估计结点n到终点的代价而真的去用A*算法本身以n为起点跑一遍然后得到实际最小代价,这样就没有意义了,引进 h ( n ) h(n) h(n)就是为了“剪支”的,如果在 h ( n ) h(n) h(n)里对n展开用A*计算,那剪支的意义何在?都已经展开了)

  构造好 f ( n ) f(n) f(n)后,构造一个优先队列(如果对效率要求不高,直接用普通的队列也可以),队列里保存活结点,每次出队的元素为扩展结点,扩展结点发散到的新结点将入队到队列。算法开始时把起点加入队列,循环直到队列为空,即可找到最优路线。如果采用优先队列,每次出队的元素为 f ( n ) f(n) f(n)值最小的结点,这样会大大减小搜索范围。在下面的搜索过程图示可以直观地感受到使用优先队列和普通队列的区别。

如何构造 h ( n ) h(n) h(n)

  要构造 h ( n ) h(n) h(n)首先要定义任意两个结点的距离,不能像Dijkstra那样用没有定义任意两点距离的抽象的图(Dijkstra算法用的图最多邻接矩阵带有权值,但即使是这样,也只是在“能直接到达的两个点之间”定义了距离,不能直接到达而是需要中转的两个点之间并没有定义距离)
  为了简化,使用格子地图,则 h ( n ) h(n) h(n)可以构造为:

一、欧氏距离

  最简单的就是直接用欧式距离估计结点n到终点的距离,这样对于格子地图必定满足 h ( n ) ≤ h ∗ ( n ) h(n) \leq h^*(n) h(n)h(n),首选。

二、曼哈顿距离

  对于规定不能斜着走的格子地图,用曼哈顿距离(两点X差的绝对值+Y差的绝对值)也是可以的。但是如果地图可以斜着走,就不能保证 h ( n ) ≤ h ∗ ( n ) h(n) \leq h^*(n) h(n)h(n)了,不过可以采用除以 2 \sqrt{2} 2 的方式保证 h ( n ) ≤ h ∗ ( n ) h(n) \leq h^*(n) h(n)h(n)必定满足。

三、其他

  其他任何满足 h ( n ) ≤ h ∗ ( n ) h(n) \leq h^*(n) h(n)h(n)的距离都可以选用。

关于 g ( n ) g(n) g(n)

  把 g ( n ) g(n) g(n)作为一个属性(比如我用cost表示这个属性)绑定在结点n里即可,寻路算法开始前设置所有的结点的cost设为 + ∞ +\infty +(实际编程里用负数表示 + ∞ +\infty +,常用-1表示)。在寻路开始时,先把起点的cost设为0,然后从起点开始发散的过程中,如果是直着(上、下、左、右)从格子A到下一个格子B,则到达的那个格子B的cost设置为A的 c o s t + 1 cost+1 cost+1,如果是斜着(左上方、左下方、右上方、右下方)从格子A到下一个格子B,则到达的那个格子B的cost设置为A的 c o s t + 2 cost+\sqrt{2} cost+2 即可完成支持直走和斜走的cost的迭代,函数 g ( n ) g(n) g(n)只需要返回n的cost即可。

路况设置如何实现

  就是上述的cost+1改成 c o s t + 1 × c o n d i t i o n cost+1 \times condition cost+1×condition,把上述的 c o s t + 2 cost+\sqrt{2} cost+2 改成 c o s t + 2 × c o n d i t i o n cost+\sqrt{2} \times condition cost+2 ×condition,其中condition是要到达的位置的路况。
  然后要修改 h ( n ) h(n) h(n),在原先返回的 h ( n ) h(n) h(n)的基础上乘整个地图的最小的condition,这样就能保证对于任何一个n仍然满足 h ( n ) ≤ h ∗ ( n ) h(n) \leq h^*(n) h(n)h(n),这样即可在设置不同路况的情况下还能保证能解得最优解。

完整的流程

  “伪代码”如下:

[准备格子地图,设置h(n)并且f(n)=g(n)+h(n)]
|
|算法开始
v
[输入起点b和终点e,设置当前已经得到的临时最优解对应的代价M=正无穷]
|
v
[构造一个优先队列Q元素为结点(Node*),结点拥有属性double cost、Node *prior和double condition,每次出队的结点n为f(n)值最小的]
|
|初始化所有结点的cost为正无穷(代码实现起来是-1),prior为NULL,condition是路况
|
v
[b入队到Q]
|
|<--------------------------------------
|                                     真|
v            真                         |        假
[Q不为空?] -------->[Q出队一个元素i]-->[f(i)>=M?]------->[i往8个方向发散,记8个方向在数组里t[8]; j=0]
|    ^                                                                        |
|    |                                      假                                 v                ++j
|    ---------------------------------------------------------------------- [j<8?] <-------------------------
|                                                                             |                             |
|假                                                                            |真                           ^
|                                                                             v                        假   |
|      [i->cost+d(i, t[j]) < t[j]->cost ?   (d(i, t[j])如果i和t[j]是直着的是1,如果i和t[j]是斜着的是根号2)]-->---|
|                                                                            |真                            |
|                                                                            v                              ^
|                          [设置t[j]->cost = i->cost+d(i, t[j])*t[j]->condition; 设置t[j]->prior = i]        |
|                                                     |                                                     |
|                                                     v                 假                                  |
|          [t[j]是e?  (如果使用的是拷贝的地图数据则考虑t[j]和e坐标是否相同)] ------>[t[j]入队到Q]------------>------
|                                          |真                                                              |
|                                          v                                                                ^
|                                 [设置M = t[j]->cost]----------------->-------------------------------------
|
|                                                            ----------------------------
|                                                            |                           |
v                 真                                         v                           |
[e->prior!=NULL?]------->[构造一个栈R,设置结点指针i=e]----->[i==NULL?]------->[i入栈到R; i=i->prior]
|                                                           真|      假
|假                                                           |
v                                                             v
[无解,b和e之间是不连通的]                         [返回R,R的出栈顺序即为从b到e的路径]

  上面的“伪代码”乱得我自己都不想看。。。下面来个Flowchart流程图表述流程吧:

Created with Raphaël 2.2.0 准备格子地图,设置h(n)并且f(n)=g(n)+h(n) 算法开始 设置当前已经得到的临时最优解对应的代价M=正无穷 输入起点b和终点e 构造一个优先队列Q元素为结点(Node*), 结点拥有属性: double cost(当前已用代价)、 Node *prior(路径的上一个位置)、 double condition(路况), 每次出队的结点n为f(n)值最小的 初始化所有结点的cost为正无穷(代码实现起来是-1), prior为NULL b入队到Q Q不为空 Q出队一个元素i f(i)<M i往8个方向发散,记8个方向在数组里t[8]。 d(x, y)表示相邻两点的距离,直着为1, 斜着为根号2; j=0 j<8 i->cost+d(i, t[j]) < t[j]->cost 设置t[j]->cost = i->cost+d(i, t[j])*t[j]->condition; 设置t[j]->prior = i t[j]是e 设置M = t[j]->cost ++j e->prior!=NULL 构造一个栈R,设置结点指针i=e i==NULL 返回R,R的出栈顺序即为从b到e的路径 结束 i入栈到R i=i->prior 无解,b和e之间是不连通的 t[j]入队到Q yes no yes no yes no yes no yes no yes no yes no

搜索过程图示

  下面展示搜索过程动画的工具是我在写完A*算法后觉得挺有意思然后进一步写的一个GUI程序,在后面的GUI程序下载链接可以下载。

允许斜走,使用优先队列

允许斜走优先队列

禁止斜走,使用优先队列

禁止斜走优先队列

允许斜走,使用普通队列

允许斜走普通队列

禁止斜走,使用普通队列

禁止斜走普通队列

  可以看到,用优先队列会减少很多不必要的搜索区域。我是先录屏,然后上面两张图片是用ps转换为gif的,下面两张因为时间比较长,用ps储存为web格式的时候内存爆了,所以下面两张是用格式工厂转换的,画质极差,将就着看吧。

核心代码

  由于代码较长,不能折叠显示,这里只贴出部分代码,完整代码见:
https://github.com/Eyre-Turing/a_star

结点展开的循环

//isRunnable是一个bool变量,如果要中止寻路,则可以通过在其他的线程把isRunnable设置为false实现。
//aliveNodeP是一个优先队列
//handleAliveNodeCallBack是一个回调函数,用于给GUI程序等提供显示路径搜索过程使用。
//MapPos是一个结构体,保存地图的一个点的坐标(r, c)、已经消耗的代价(cost)以及代价估计(lowerBound)
//searchOne是把一个结点往四周展开(如果支持斜着走就是八方向展开,否则是四方向展开)
//matrix是地图,其get方法是获取一个结点指针(MapNode*),MapNode的isInAliveNodes属性保存该结点的坐标(MapPos)是否在aliveNodeP里存在,目的是去除重复展开。
while(isRunnable && !aliveNodeP.empty())
{MapPos handleAliveNode = aliveNodeP.top();aliveNodeP.pop();if(handleAliveNodeCallBack){handleAliveNodeCallBack(this, handleAliveNode);}searchOne(handleAliveNode.r, handleAliveNode.c, handleAliveNode.lowerBound);matrix->get(handleAliveNode.r, handleAliveNode.c)->isInAliveNodes = false;
}
while(!aliveNodeP.empty())
{MapPos handleAliveNode = aliveNodeP.top();aliveNodeP.pop();matrix->get(handleAliveNode.r, handleAliveNode.c)->isInAliveNodes = false;
}

代价估计函数 f ( n ) f(n) f(n)

/** 从起点经过当前点[r][c],再到终点,路径长度的一个下界* 如果这个下界小于当前已经得到的解,认为是有希望比当前已经得到的解更优的* 如果这个下界大于或等于已经得到的解,则这条路不必再展开搜索了* -1认为是正无穷(或者是负数认为是正无穷)*/
double AStar::lowerBoundFunction(int r, int c) const
{
//minCondition是地图里最小的路况,如果最小路况为正无穷(代码表示为负数),就返回正无穷(代码表示为-1)。
//endR和endC为终点的位置坐标,endR为Y值,endC为X值。if(minCondition < 0){return -1;}if(isObliqueEnable){return matrix->get(r, c)->cost+sqrt(pow(r-endR, 2)+pow(c-endC, 2))*minCondition;    //下界为:已走路程+最小路况下当前点到终点的欧氏距离}else{return matrix->get(r, c)->cost+(fabs(r-endR)+fabs(c-endC))*minCondition;            //下界为:已走路程+最小路况下当前点到终点的曼哈顿距离}
}

GUI程序下载链接

Windows:  http://eyre-turing.top/project/get_data/a_star.exe
Linux x64: http://eyre-turing.top/project/get_data/a_star

给两幅测试地图

一、点我下载简单迷宫

  效果如下:
简单迷宫

二、点我下载复杂迷宫

  效果如下:
复杂地图

该地图我没有设置起点和终点位置,你可以自己随便设置。

GUI程序使用说明

  宽度高度编辑框设置地图的大小,尺寸设置每个格子的边长占多少个像素。修改了这些参数后要点击确认修改才会生效。
  勾选编辑模式即可编辑墙壁以及路况,编辑模式下在地图空白处点击左键即可添加墙,在墙处点击左键即可移除墙(地图界面中黑色的是墙)。编辑模式下,对空白处右键即可设置路况,右键后选中的格子会有黄色边框,黄色边框出现后,在路况编辑框填入数字即可调整选中格子的路况,路况值越大表示该位置越拥堵。
  点击设置起点后即可在地图上标记起点位置,起点是绿色格子;点击设置终点后即可在地图上标记终点位置,终点是红色格子。
  勾选显示网格后会画出地图所有格子的边框。
  勾选允许斜走即可八方向发散搜索路线,勾选斜可贴墙即可斜走的情况下贴着墙走。

  点击计算路线即可开始运行A*算法搜索路径,点击清理路线即可消除计算出来的路线,在开始寻路的时候,清理路线按钮会变成计算中止按钮,点击即可中止寻路。
  勾选画出路线即可在搜索出路径后在地图上画出来。
  勾选优先队列即使用优先队列来保存活结点队列。
  勾选显示过程即可在计算路线时动画显示出搜索路线的过程,勾选后会在下方显示一个文本编辑框,该编辑框可以设置动画显示的速度。
  菜单栏的文件可以展开以执行读取地图和保存地图的操作。


http://chatgpt.dhexx.cn/article/InODpFv1.shtml

相关文章

A星算法优化(一)启发函数

基于Python语言对A星算法进行优化&#xff1a;(视频中会拿python与matlab作对比) 源码地址&#xff1a;https://github.com/Grizi-ju/ros_program/blob/main/path_planning/Astar.py B站详解视频&#xff1a;https://www.bilibili.com/video/BV1FL4y1M7PM?spm_id_from333.999…

猴子都能看懂的A星算法原理

文章目录 A星算法基本原理什么是寻路算法算法的思路 算法实现脚本1————cconst.cs脚本2————AStar.cs Unity演示演示样例一演示样例二演示样例三演示样例四 俗话说&#xff0c;好记性不如烂笔头&#xff0c;对于有了解过寻路算法的同学&#xff0c;对于A星算法应该不陌生…

A星寻路算法详解(C++实现 完整代码+图片演示 )

文章目录 三种寻路算法 A星寻路算法A星寻路算法思想A星寻路准备A星寻路过程&#xff08;图例&#xff09;A星寻路代码&#xff08;完整&#xff09; 三种寻路算法 深度寻路算法&#xff1a;不一定能找到最佳路径&#xff0c;但是寻路快速&#xff0c;只能走直线。广度寻路算法…

A星(A*、A Star)路径规划算法详解(附MATLAB代码)

首先看看运行效果&#xff0c;分别有三种模式&#xff0c;代码运行前需要通过鼠标点击设置起点和终点。 第一种模式直接输出最短路径 第二种模式输出最短路径的生成过程 第三种模式输出最短路径的生成过程和详细探索的过程 代码获取 gitee链接&#xff1a;https://gitee.c…

什么是脏读?不可重复读?幻读?如何解决?

什么是脏读&#xff1f;不可重复读&#xff1f;幻读&#xff1f;如何解决&#xff1f; 朋友最近面试美团&#xff0c;被面试官问到数据库的幻读问题&#xff0c;自己正好最近复习到这&#xff0c;做个笔记整理一下数据库的三大特征以及隔离级别。 一.先来回顾一下什么是事务&…

MySQL理论:脏读、不可重复读、幻读

&#x1f3c6;今日学习目标&#xff1a; &#x1f340;MySQL理论&#xff1a;脏读、不可重复读、幻读 ✅创作者&#xff1a;林在闪闪发光 ⏰预计时间&#xff1a;30分钟 &#x1f389;个人主页&#xff1a;林在闪闪发光的个人主页 &#x1f341;林在闪闪发光的个人社区&#xf…

数据库事务隔离级别(脏读、幻读、不可重复读)

一、脏读、幻读和不可重复读 一、脏读、不可重复读、幻读 1、脏读&#xff1a;脏读就是指当一个事务正在访问数据&#xff0c;并且对数据进行了修改&#xff0c;而这种修改还没有提交到数据库中&#xff0c;这时&#xff0c;另外一个事务也访问这个数据&#xff0c;然后使用了…

快速理解脏读,不可重复读,幻读

介绍 要聊事务&#xff0c;不可避免的要提到数据库事务的四大特性&#xff1a;ACID atomicconsistenceisolationdurability 先放一个表格&#xff0c;看看4个隔离级别会出现的各种问题&#xff0c;网上的解释一大堆。看完后还是一脸懵逼&#xff0c;感觉懂了&#xff0c;又好…

MySQL之脏写、脏读、不可重复读、幻读

脏写和脏读都是在多个事务同时修改或读取同一行数据的情况下产生的问题。比如现在有事务1和事务2同时对一行数据进行修改&#xff0c;事务1将值改成1&#xff0c;而事务2将值改成了2&#xff0c;这时的值应该是2&#xff0c;但是就在这时&#xff0c;事务1发生了回滚&#xff0…

数据库必备知识:脏读和幻读的定义及应对策略

随着数据库应用的广泛使用&#xff0c;数据库并发性和一致性的问题成为了引起重视的问题之一。其中&#xff0c;脏读&#xff08;Dirty Read&#xff09;和幻读&#xff08;Phantom Read&#xff09;是常见的并发访问问题&#xff0c;本文将对脏读、幻读进行详细介绍&#xff0…

Seata AT模式怎样防止脏写和脏读

前言 Seata AT 模式是一种非侵入式的分布式事务解决方案&#xff0c;Seata 在内部做了对数据库操作的代理层&#xff0c;我们使用 Seata AT 模式时&#xff0c;实际上用的是 Seata 自带的数据源代理 DataSourceProxy&#xff0c;Seata 在这层代理中加入了很多逻辑&#xff0c;…

mysql 脏数据是什么_什么是脏读?

什么是脏读&#xff1f; 脏读又称无效数据的读出&#xff0c;是指在数据库访问中&#xff0c;事务T1将某一值修改&#xff0c;然后事务T2读取该值&#xff0c;此后T1因为某种原因撤销对该值的修改&#xff0c;这就导致了T2所读取到的数据是无效的&#xff0c;值得注意的是&…

[Database] 脏读、幻读这些都是什么?事务隔离级别又是什么?MySQL数据库的事务隔离级别都有哪些?

文章目录 前言事务隔离级别三种数据不一致问题1. 脏读2. 不可重复读3. 幻读不可重复读 vs 幻读 四种事务隔离级别1. READ UNCOMMITTED2. READ COMMITTED3. REPEATABLE READ4. SERIALIZABLE 不同事务隔离级别会面临的问题不同隔离事务级别的使用率排名 实战查看事务隔离级别更改…

Mysql-详解脏读、不可重复读、幻读

Mysql的事务隔离级别 Mysql有四种事务隔离级别&#xff0c;这四种隔离级别代表当存在多个事务并发冲突时&#xff0c;可能出现的脏读、不可重复读、幻读的问题。 脏读 大家看一下&#xff0c;我们有两个事务&#xff0c;一个是 Transaction A&#xff0c;一个是 Transaction B…

MySQL的事务,脏读,不可重复读,幻读

一、什么是事务 在MySQL中&#xff0c;事务是一种机制、一个操作序列&#xff0c;是访问和更新数据库的程序执行单元。事务中包含一个或多个数据库操作命令&#xff0c;会把所有的命令作为一个整体一起向系统提交或撤销操作请求&#xff0c;即这一组数据库命令要么都执行&#…

脏读、幻读、不可重复读,傻傻分不清楚

脏读 &#xff08;针对未提交数据) 脏读又称无效数据读出&#xff08;读出了脏数据&#xff09;。一个事务读取另外一个事务还没有提交的数据叫脏读。 例如&#xff1a;事务T1修改了某个表中的一行数据&#xff0c;但是还没有提交&#xff0c;这时候事务T2读取了被事务T1修改…

【MySQL理论】脏读、不可重复读、幻读

文章目录 1. 脏读(dirty read)脏读是指事务读取到其他事务未提交的数据 2. 不可重复读(non-repeatable read)不可重复读是指在同一次事务中前后查询不一致的问题 3. 幻读(phantom read)幻读是一次事务中前后数据量发生变化&#xff0c;用户产生不可预料的问题 4. 不可重复读和幻…

脏读、不可重复读、幻读、丢失更新

根儿上来说&#xff0c;为什么需要事务和锁&#xff1f; 如果所有的操作都是依次进行的&#xff0c;或者说mysql的server是单线程的&#xff0c;就不会有并发问题&#xff0c;也就不需要事务和锁了。然而事实上&#xff0c;是多客户端&#xff0c;多线程的&#xff0c;所有必须…

一文搞懂MySQL脏读,幻读和不可重复读

目录 MySQL 中事务的隔离 1.READ UNCOMMITTED2.READ COMMITTED3.REPEATABLE READ4.SERIALIZABLE前置知识 1.事务相关的常用命令2.MySQL 8 之前查询事务的隔离级别3.MySQL 8 之后查询事务的隔离级别4.查看连接的客户端详情5.查询连接客户端的数量6.设置客户端的事务隔离级别7.新…

脏读、幻读和不可重复读

一、引言 脏读、不可重复读和幻读是数据库中由于并发访问导致的数据读取问题。当多个事务同时进行时可以通过修改数据库事务的隔离级别来处理这三个问题。 二、问题解释 1、脏读&#xff08;读取未提交的数据&#xff09; 脏读又称无效数据的读出&#xff0c;是指在数据库访…