【小白学算法】A-star(A*) 搜索算法超详细解析+例题[洛谷]P1379八数码难题

介绍

A-star(A*)算法是一种经典的启发式搜索算法,用来在图或状态空间中找到从起点到终点的代价最小路径。它结合了Dijkstra算法和贪心算法的优点,通过启发式函数在保证最优解的同时提高搜索效率。

核心思想

A*算法的目标是找到从起点到终点的最短路径。其通过维护一个优先队列(最小堆),根据估价函数\(f(n)\)来选择下一步要探索的节点,其中估价函数由两部分组成:

  • 实际代价\(g(n)\):从起点到当前节点\(n\)的已知路径代价(已经走了多少步)。

  • 启发式代价\(h(n)\):从当前节点\(n\)到终点的估计代价(预测还要多少步该部分是A*算法的“聪明之处”,它会引导算法朝向重点前进,从而减少需要探索的节点数量)。

总估价函数为:

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

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;
}
posted @ 2025-08-11 02:06  芝士青瓜不拿铁  阅读(239)  评论(0)    收藏  举报