【小白学算法】A-star(A*) 搜索算法超详细解析+例题[洛谷]P1379八数码难题
介绍
A-star(A*)算法是一种经典的启发式搜索算法,用来在图或状态空间中找到从起点到终点的代价最小路径。它结合了Dijkstra算法和贪心算法的优点,通过启发式函数在保证最优解的同时提高搜索效率。
核心思想
A*算法的目标是找到从起点到终点的最短路径。其通过维护一个优先队列(最小堆),根据估价函数\(f(n)\)来选择下一步要探索的节点,其中估价函数由两部分组成:
-
实际代价\(g(n)\):从起点到当前节点\(n\)的已知路径代价(已经走了多少步)。
-
启发式代价\(h(n)\):从当前节点\(n\)到终点的估计代价(预测还要多少步该部分是A*算法的“聪明之处”,它会引导算法朝向重点前进,从而减少需要探索的节点数量)。
总估价函数为:
A*算法每次选择\(f(n)\)最小的节点进行扩展,直到找到终点。
算法步骤
初始化
-
创建一个开放列表存储待探索节点,初始只包含起点。
-
创建一个关闭列表存储一个已探索的节点,初始为空。
-
为起点设置\(g(start)\)=0 ,\(h(start)\) 根据启发式函数计算,\(f(start)=h(start)\)。
主循环
从开放列表中选取\(f(n)\)最小的节点作为当前节点,如果当前节点是终点,算法结束,重建路径,否则将当前节点从开放列表中移除,加入关闭列表,检查当前节点的所有邻居节点,如果邻居节点在关闭列表里,则跳过,否则计算邻居节点的\(g(n)\)(从起点到邻居的路径代价),如果该邻居节点不在开放列表中,或者新的g(n)值更小,则更新他的g(n)值,并计算它的f值,将其父节点设置为n,然后将他加入到开放列表中,当循环结束时没有找到终点,则表示路径不存在。
启发式函数\(h(n)\)
启发式函数的选择决定了A*算法的效率,但启发式函数必须满足一个条件:它必须是可接受的。什么意思呢,就是对于图中的任何节点n,它到终点的预估代价从不大于其实际最短路径的代价,即:h(n)≤从节点n到终点的实际最短路径成本;
如果启发式函数是可接受的,那么A*算法就能保证找到最优解(即最短路径),如果启发函数不可接受,算法可能会更快,但无法保证找到最优解。
在不同的应用中,启发函数有多种选择,eg:
曼哈顿距离: 适用于网格状的地图,只能水平或垂直移动。\(h(n)=|x_n-x_{终点}|+|y_n-y_{终点}|\)
欧几里得距离: 适用于可以沿任意方向移动的地图。\(h(n)=\sqrt{(x_n−x_{终点})^2+(y_n−y_{终点})^2}\)
A* 算法的优缺点
优点:
-
高效:相比于 Dijkstra 算法,A* 算法由于其启发式搜索,通常能更快地找到路径,尤其是在大型图中。
-
保证最优解:当使用可接受的启发式函数时,A* 算法能确保找到最短路径。
-
通用性:可以应用于各种图搜索问题(如网格,加权图等)。
缺点:
-
内存消耗:需要存储开放列表和关闭列表中的所有节点,当图非常大时,可能会占用大量内存。
-
对启发式函数的依赖:性能高度依赖于启发式函数的质量。一个糟糕的启发式函数可能导致算法性能下降,甚至退化为 Dijkstra 算法。
例题_洛谷P1379 八数码难题
题目地址:https://www.luogu.com.cn/problem/P1379
题目分析:
将0~8共九个数字放入三宫格中,0代表空,即0所在位置可与周围正方向的数字交换,给一个三宫格状态,找到其变为目标状态的最短交换次数,是一个最短路径搜索问题,可以用bfs或A*求解,但是bfs虽然能保证找到最短路径,但会盲目扩展很多无用的状态,效率低,利用启发式函数h(n)搜索更高效。
解题思路
将0作为起点,坐标存入关闭列表中,剩余与目标状态不同的位置个数为h(n),用优先队列小根堆记录,g表示当前路径长度(已走的步数),h为启发式函数(估算剩余步数),将初始状态入队,g=0,h根据初始状态计算,每次取出f最小的状态,如果等于目标状态,则找到最短路径,输出g即可,否则找到当前状态下0的位置,继续枚举其四个方向,生成新状态并入队(未访问过的,入队时个g+1),重复步骤直到找到目标状态,若循环结束依然没找到,则说明没有可达路径(无解)。
题解代码
#include<iostream>
#include<set>
#include<queue>
using namespace std;
struct matrix{
int a[5][5];//定义一个矩阵
bool operator<(const matrix &other) const{
for(int i=1;i<=3;i++){
for(int j=1;j<=3;j++){
if(a[i][j]!=other.a[i][j]){
return a[i][j]<other.a[i][j];
}
}
}
return false;//完全相同则返回false
}//定义小于运算符,用于对set去重,按照字典序比较矩阵
}start_state,goal_state;//初始状态(控制台输入),目标状态(自定义)
int h(const matrix &m){//启发式函数h:计算与目标状态不一致的非零数字的个数(曼哈顿距离的简化版)
int cnt=0;//个数初始为0
for(int i=1;i<=3;i++){
for(int j=1;j<=3;j++){
if (m.a[i][j]!=goal_state.a[i][j]&&m.a[i][j]!=0){//如果与目标状态的该位置数字不同,则计数加1
cnt++;
}
}
}
return cnt;
}
struct Node{//定义A*的搜索节点
matrix mat;//当前的状态
int g;//已走的步数
bool operator<(const Node &other) const{
return g+h(mat)>other.g+h(other.mat);
}//定义优先队列的比较规则:按f=g+h从小到大排序
};
priority_queue<Node> pq;//优先队列,按照f的值进行排列
set<matrix> judge;//用set存储已访问过的状态,防止重复
int ulx,uly;//用于存0的位置坐标
char ch;
int dx[4]={0,0,1,-1};
int dy[4]={1,-1,0,0};//上下左右四个方位坐标变化,用于遍历判断
int main(){
goal_state.a[1][1]=1;
goal_state.a[1][2]=2;
goal_state.a[1][3]=3;
goal_state.a[2][1]=8;
goal_state.a[2][2]=0;
goal_state.a[2][3]=4;
goal_state.a[3][1]=7;
goal_state.a[3][2]=6;
goal_state.a[3][3]=5;
//自定义目标状态矩阵
for(int i=1;i<=3;i++){
for(int j=1;j<=3;j++){
cin>>ch;
start_state.a[i][j]=ch-'0';//用字符输入后转为数字,能吸收空格和换行,
//方便处理无空位输入,eg123450678,直接输入int类型会导致将其读为一整个数字
}
}//
judge.insert(start_state);//将初始状态标记
pq.push({start_state,0});//初始状态入队列
while(!pq.empty()){
Node test=pq.top();//取出队顶用于判断四周
pq.pop();
if(h(test.mat)==0){//若此时启发式函数值为0,说明达到目标状态,输出步数并结束程序即可
cout<<test.g<<endl;
return 0;
}
for(int i=1;i<=3;i++){
for(int j=1;j<=3;j++){
if(test.mat.a[i][j]==0){
ulx=i,uly=j;//找到该状态下0的位置并记录
}
}
}
for(int i=0;i<4;i++){//遍历0的四个方向
int nx=ulx+dx[i];
int ny=uly+dy[i];
if(nx>=1&&nx<=3&&ny>=1&&ny<=3){//判断下标合法
swap(test.mat.a[ulx][uly],test.mat.a[nx][ny]);//按要求交换数字
if(!judge.count(test.mat)){//若交换后的状态未入队列过
judge.insert(test.mat);//那么入队列并标记
pq.push({test.mat,test.g+1});
}
swap(test.mat.a[ulx][uly],test.mat.a[nx][ny]);//入队列后复原位置
}
}
}
return 0;
}

浙公网安备 33010602011771号