跳转至

A*

1. 🗺️ A* 算法

A* (A-Star) 是一种用于求 单源最短路径(特别是已知终点时)的启发式搜索算法。 意思是:

在庞大的地图或状态空间中,寻找从起点到终点的最优路线。 例如:

游戏中 NPC 自动寻路 地图导航的最短路线 解决八数码(拼图)复原 都可以用 A*。

2. ✅ 适用条件

A* 适用于:

边权值非负(与 Dijkstra 一样,不能有负边) 明确知道起点和终点 搜索空间巨大,普通盲目搜索会超时或超内存 只要能为目标设计出合理的“预估距离”,一般可用 A*。

3. 🧠 核心思想

每次从当前未确定的点中,选出:

综合预估总代价最小的点 然后用它去扩展周围的点。

核心公式: \(F = G + H\)

\(G\):从起点走到当前点的实际代价 \(H\):从当前点走到终点的预估代价(启发函数) \(F\):总评估代价 优先选择 \(F\) 值最小的节点向外扩展。

4. 📏 启发函数 (H值)

这是 A* 区别于其他算法的灵魂。

常见选法:

曼哈顿距离(网格图,只能上下左右四向移动) 欧几里得距离(可以任意方向移动的直线距离) 切比雪夫距离(网格图,允许八向移动)

5. 🍎 举例理解

网格图找路,起点到终点中间有堵墙:

Dijkstra:像水波一样四面八方均匀蔓延,直到碰到终点。 A*:像有雷达一样,直奔终点方向走,碰到墙再尝试绕路。 因为 \(H\) 值的存在,算法有了明确的“方向感”。

6. ⚙️ 基础逻辑 (Open/Closed表)

Open 表(待检查队列):

存放发现了、但还没扩展邻居的点。 Closed 表(已检查集合):

存放已经彻底扩展完的点。 过程:

每次从 Open 表拿出 \(F\) 最小的点,放入 Closed 表。 检查它的邻居,计算新的 \(G\)\(F\) 放入 Open 表。

7. 💻 堆优化模板(竞赛常用)

适合网格寻路、状态空间搜索。 使用:

priority_queue 维护最小 \(F\) 值。

#include <iostream>
#include <queue>
#include <vector>
using namespace std;

const int INF = 1e9;

struct Node {
    int x, y, g, h, f;
    bool operator<(const Node& a) const {
        return f > a.f; // 优先队列,F值小的优先出队
    }
};

void astar(Node start, Node end) {
    priority_queue<Node> q;

    start.g = 0;
    start.h = get_h(start, end); // 计算启发值
    start.f = start.g + start.h;
    q.push(start);

    while(!q.empty()) {
        Node u = q.top();
        q.pop();

        if(u.x == end.x && u.y == end.y) {
            // 找到终点
            return;
        }

        if(vis[u.x][u.y]) continue;
        vis[u.x][u.y] = true;

        for(auto next : get_neighbors(u)) {
            int new_g = u.g + cost(u, next);

            if(new_g < dis[next.x][next.y]) {
                dis[next.x][next.y] = new_g;
                next.g = new_g;
                next.h = get_h(next, end);
                next.f = next.g + next.h;
                q.push(next);
            }
        }
    }
}
#include <iostream>
#include <queue>
#include <vector>
#include <cstring>
#include <cmath>
using namespace std;

const int INF = 1e9;
const int N = 105;

int n = 5, m = 5;          // 地图大小
bool vis[N][N];            // 是否访问过
int dis[N][N];             // 最短距离
int mp[N][N];              // 地图,0可走,1障碍

struct Node {
    int x, y, g, h, f;
    bool operator<(const Node& a) const {
        return f > a.f;   // f小的优先
    }
};

/// 启发函数:曼哈顿距离
int get_h(Node a, Node b) {
    return abs(a.x - b.x) + abs(a.y - b.y);
}

/// 移动代价:走一步花费1
int cost(Node a, Node b) {
    return 1;
}

/// 获取邻居(上下左右)
vector<Node> get_neighbors(Node u) {
    vector<Node> res;

    int dx[4] = {1,-1,0,0};
    int dy[4] = {0,0,1,-1};

    for(int i=0;i<4;i++) {
        int nx = u.x + dx[i];
        int ny = u.y + dy[i];

        if(nx < 1 || nx > n || ny < 1 || ny > m) continue;
        if(mp[nx][ny] == 1) continue; // 障碍物不能走

        res.push_back({nx, ny, 0, 0, 0});
    }

    return res;
}

void astar(Node start, Node end) {
    priority_queue<Node> q;

    memset(vis, false, sizeof(vis));

    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            dis[i][j] = INF;

    start.g = 0;
    start.h = get_h(start, end);
    start.f = start.g + start.h;

    dis[start.x][start.y] = 0;
    q.push(start);

    while(!q.empty()) {
        Node u = q.top();
        q.pop();

        if(u.x == end.x && u.y == end.y) {
            cout << "最短距离 = " << u.g << endl;
            return;
        }

        if(vis[u.x][u.y]) continue;
        vis[u.x][u.y] = true;

        for(auto next : get_neighbors(u)) {
            int new_g = u.g + cost(u, next);

            if(new_g < dis[next.x][next.y]) {
                dis[next.x][next.y] = new_g;

                next.g = new_g;
                next.h = get_h(next, end);
                next.f = next.g + next.h;

                q.push(next);
            }
        }
    }

    cout << "无法到达终点\n";
}

int main() {
    Node start = {1,1,0,0,0};
    Node end   = {5,5,0,0,0};

    mp[3][3] = 1; // 障碍物

    astar(start, end);

    return 0;
}

示例迷宫

#include <bits/stdc++.h>
using namespace std;

struct Node{
    int x,y,g,h,f;
    bool operator<(const Node& a)const{
        return f>a.f;
    }
};

int n,m;
char mp[105][105];
int dis[105][105];
bool vis[105][105];

int dx[4]={1,-1,0,0};
int dy[4]={0,0,1,-1};

int h(int x,int y,int ex,int ey){
    return abs(x-ex)+abs(y-ey);
}

int astar(int sx,int sy,int ex,int ey){
    priority_queue<Node> q;
    memset(dis,0x3f,sizeof dis);

    q.push({sx,sy,0,h(sx,sy,ex,ey),h(sx,sy,ex,ey)});
    dis[sx][sy]=0;

    while(!q.empty()){
        Node u=q.top(); q.pop();

        if(vis[u.x][u.y]) continue;
        vis[u.x][u.y]=1;

        if(u.x==ex&&u.y==ey) return u.g;

        for(int i=0;i<4;i++){
            int nx=u.x+dx[i];
            int ny=u.y+dy[i];

            if(nx<1||ny<1||nx>n||ny>m) continue;
            if(mp[nx][ny]=='#') continue;

            int ng=u.g+1;

            if(ng<dis[nx][ny]){
                dis[nx][ny]=ng;
                int hh=h(nx,ny,ex,ey);
                q.push({nx,ny,ng,hh,ng+hh});
            }
        }
    }
    return -1;
}

8. 🚀 为什么比 Dijkstra 快

Dijkstra 属于盲目搜索:

会探索很多背离终点的无用方向。 A* 带有启发性:

优先探索离终点“看起来更近”的方向,在广阔地图中遍历的节点数量大幅减少。

9. 🎯 常见题型

迷宫寻路 起点到终点的最少步数(带复杂障碍物)。 状态压缩与转化 八数码(15数码)问题求最少还原步数。 K短路问题 A* 配合其他图论算法求前 K 条最短路径。

10. ⚠️ 常见错误

H值高估实际距离

如果预估的 \(H\) 大于实际距离,A* 可能找不到最优解,退化为普通贪心。 记住:\(H\) 值必须小于或等于实际代价(可采纳性)。 没有使用优先队列

每次找最小 \(F\) 变成 \(O(n)\),导致严重超时。 未处理节点更新

如果在 Open 表中发现同一个点有更短的 \(G\) 值,必须用新的状态更新入堆。

11. 🔍 如何判断使用 A*

看到这些关键词:

单起点到单终点(明确了具体目标) 地图/网格寻路 状态转移求最少步数 普通 BFS / Dijkstra 空间或时间爆炸 通常考虑 A*。

12. 💡 一句话理解

A* = Dijkstra 的求稳(实际走过的距离 \(G\)) + 贪心算法的直觉(预估剩下的距离 \(H\))。

13. 📝 总结

A* 是人工智能和游戏开发中最强大的寻路算法之一。 记住三点:

用于单点到单点最优解 核心公式 \(F = G + H\) \(H\) 值绝对不能高估实际距离

接下来我们可以朝哪个方向继续探索? 1. 深入启发函数: 探讨在不同类型的网格中,如何准确计算曼哈顿距离或切比雪夫距离。 2. 对比与演进: 仔细剖析 A 和 Dijkstra 在遍历底层逻辑上的直观差异。 3. 状态空间模型: 看看如何把经典的“八数码拼图”转化为 A 可以求解的图论问题。