A*(A-star)算法

概述

A*(A-star)算法是一种在图中寻找从初始节点到目标节点最短路径的启发式搜索算法。它结合了Dijkstra算法的确保性(保证找到一条最短路径)和贪心算法的高效性(快速找到目标)。A*算法通过评估函数f(n) = g(n) + h(n)来工作,其中g(n)是从起始点到任何顶点n的实际成本,而h(n)是从顶点n到目标的估计最低成本,通常用启发式函数来计算,这个函数需要事先设计来反映实际的地形或环境特征。

A*算法具有以下显著特性:

  1. 最优性:当启发式函数h(n)满足某些条件时,A*算法能保证找到一条最低成本路径。
  2. 效率:A*算法在执行过程中保持高效,特别是当启发式函数能够较好地估计到目标的成本时。

A*算法广泛应用于各类路径规划问题,如机器人导航、地图定位服务和游戏中的AI路径寻找等场景。通过适当选择和调整启发式函数,A*算法能够在复杂的环境中有效地寻找最短路径,同时保持计算上的可行性和效率。

启发式函数的选择对A*算法的性能和准确性至关重要。理想情况下,h(n)不应该高估实际的成本,这种情况下,A*算法保证找到一条最低成本路径。常见的启发式函数包括曼哈顿距离、欧几里得距离和对角线距离等。

关于距离

曼哈顿距离

哈顿距离(Manhattan Distance),也称为L1距离或城市街区距离(City Block Distance),是一种度量两个点在标准坐标系上的绝对轴距总和的距离。在二维空间中,如果有两个点P1(x1, y1)和P2(x2, y2),它们之间的曼哈顿距离定义为:

曼哈顿距离 = ∣ x 2 − x 1 ∣ + ∣ y 2 − y 1 ∣ 曼哈顿距离=\left | x2-x1 \right | + \left | y2-y1 \right | 曼哈顿距离=∣x2−x1∣+∣y2−y1∣

这个名称来源于纽约市的街道布局,因为曼哈顿街道呈网格状,从一个街区到另一个街区的最短路径通常是沿着街道走,而不是直线穿越建筑物。

欧几里得距离

欧几里得距离(Euclidean Distance),也称为欧氏距离或L2距离,是度量两点在欧几里得空间中直线距离的一种方法。它是最直观的距离度量方式,即两点之间的直线段长度。

在二维空间中,如果有两个点P1(x1, y1)和P2(x2, y2),它们之间的欧几里得距离定义为:

欧几里得距离 = ( x 2 − x 1 ) 2 + ( y 2 − y 1 ) 2 欧几里得距离=\sqrt{(x2−x1)^2+(y2−y1)^2} 欧几里得距离=(x2−x1)2+(y2−y1)2

这个公式可以推广到更高维度的空间中。对于n维空间中的两个点P1(p1_1, p1_2, ..., p1_n)和P2(p2_1, p2_2, ..., p2_n),它们之间的欧几里得距离为:
欧几里得距离 = ( p 2 1 − p 1 1 ) 2 + ( p 2 2 − p 1 2 ) 2 + ⋯ + ( p 2 n − p 1 n ) 2 欧几里得距离=\sqrt{(p2_1−p1_1)^2+(p2_2−p1_2)^2+\cdots+(p2_n−p1_n)^2} 欧几里得距离=(p21−p11)2+(p22−p12)2+⋯+(p2n−p1n)2

欧几里得距离是最直观和常用的距离度量方式,因为它符合我们对"距离"的直观理解。然而,在某些情况下,如城市街区或室内导航,直线距离可能不是最佳的距离度量方式,这时可能会使用曼哈顿距离或其他距离度量方式。

对角线距离

对角线距离,有时也被称为切比雪夫距离(Chebyshev Distance),是一种在多维空间中度量两点之间距离的方法。它基于这样一个概念:在棋盘上,一个棋子从一个方格移动到对角线相对的方格所需的步数。在二维空间中,如果有两个点P1(x1, y1)和P2(x2, y2),它们之间的对角线距离定义为:

对角线距离 = m a x ( ∣ x 2 − x 1 ∣ , ∣ y 2 − y 1 ∣ ) 对角线距离=max(\left |x2−x1\right | ,\left |y2−y1\right |) 对角线距离=max(∣x2−x1∣,∣y2−y1∣)

这个定义可以推广到n维空间中。对于n维空间中的两个点P1(p1_1, p1_2, ..., p1_n)和P2(p2_1, p2_2, ..., p2_n),它们之间的对角线距离为:

对角线距离 = m a x ( ∣ p 2 1 − p 1 1 ∣ , ∣ p 2 2 − p 1 2 ∣ , ⋯   , ∣ p 2 n − p 1 n ∣ ) 对角线距离=max(\left |p2_1−p1_1 \right|,|p2_2−p1_2|,\cdots,|p2_n−p1_n|) 对角线距离=max(∣p21−p11∣,∣p22−p12∣,⋯,∣p2n−p1n∣)

对角线距离的一个主要优点是它在处理具有不同尺度或单位的维度时相对简单和直观。然而,它可能不如欧几里得距离那样直观,因为它不考虑维度之间的相互作用或角度。在实际应用中,选择哪种距离度量方式取决于具体问题的性质和需求。

算法思想

A*算法是对广搜的改进。回顾一下广搜的思路,从起点开始一圈一圈的向外搜索,直到找到目标节点。在这个过程中其实做了很多无用功,而且当地图的规模扩大时,其性能也极度下降。

以下面这张图为例,绿色格子是起点,红色格子是终点,黑色是障碍物。

虽然中间有遮挡,但是我们很容易知道,大方向是向右的。所以,广搜的过程中向左扩展的部分其实就是无用功。我们应该优先寻找右边的路。

那么如何让扩展出来的格子之间产生优先级呢?这时A*算法的f(n)g(n)h(n)函数就派上了用场。f(n) = g(n) + h(n)

  • f(n):总代价
  • g(n):从起点到当前节点的实际代价
  • h(n):从当前节点到终点的预期代价

A*算法通过引入预期代价从而包含了终点的方向信息。这时候我们就可以从候选节点中选一个总代价最小的节点优先扩展。从代码实现上来说也只需要用优先队列替换原本广搜的普通队列即可。

A*算法的步骤可以概括如下:

  1. 初始化
    • 创建两个集合:开放集(Open Set)和关闭集(Closed Set)。
    • 将起始节点加入开放集,并将起始节点的g(n)值设为0,h(n)值根据启发式函数计算。
    • 将起始节点的父节点设为null。
  2. 循环执行以下步骤,直到开放集为空或找到目标节点
    • 选择节点 :从开放集中选择具有最低f(n)值的节点,即f(n) = g(n) + h(n)
    • 检查目标:如果选择的节点就是目标节点,则路径已找到,算法结束。
    • 移动节点:将选择的节点从开放集移至关闭集。
    • 扩展节点 :对于选择的节点的每一个邻居节点:
      • 如果邻居节点不在关闭集中,计算它的g(n)值(从起始节点到邻居节点的成本),并计算f(n)值。
      • 如果邻居节点不在开放集中,将其添加到开放集中,并记录当前节点为其父节点。
      • 如果邻居节点已在开放集中,但通过当前节点到达它的g(n)值更低,则更新它的g(n)值和父节点。
    • 更新启发式函数 :对于每个新加入开放集的节点或更新了g(n)值的节点,重新计算它的f(n)值。
  3. 路径重建
    • 一旦找到目标节点,从目标节点开始,通过其父节点回溯到起始节点,构建出完整的路径。
  4. 输出路径
    • 输出从起始节点到目标节点的路径。

代码实现

卡码网:126. 骑士的攻击为例

题目描述

在象棋中,马和象的移动规则分别是"马走日"和"象走田"。现给定骑士的起始坐标和目标坐标,要求根据骑士的移动规则,计算从起点到达目标点所需的最短步数。

棋盘大小 1000 x 1000(棋盘的 x 和 y 坐标均在 [1, 1000] 区间内,包含边界)

输入描述

第一行包含一个整数 n,表示测试用例的数量,1 <= n <= 100。

接下来的 n 行,每行包含四个整数 a1, a2, b1, b2,分别表示骑士的起始位置 (a1, a2) 和目标位置 (b1, b2)。

输出描述

输出共 n 行,每行输出一个整数,表示骑士从起点到目标点的最短路径长度。

输入示例

plain 复制代码
6
5 2 5 4
1 1 2 2
1 1 8 8
1 1 8 7
2 1 3 3
4 6 4 6

输出示例

plain 复制代码
2
4
6
5
1
0

通过代码:

cpp 复制代码
#include <iostream>
#include <queue>
#include <cmath>
#include <cstring>

using namespace std;

int moves[1001][1001];                                                    // 相当于开放集
bool close[1001][1001];                                                   // close列表
int dir[8][2] = {-2, -1, -2, 1, -1, 2, 1, 2, 2, 1, 2, -1, 1, -2, -1, -2}; // 移动方向

struct Node
{
    int x, y;    // 节点位置
    int f, g, h; // 总成本,到当前节点的实际成本,到目标节点的预估成本

    Node() {}

    Node(int a, int b) : x(a), y(b) {}

    bool operator<(const Node &other) const
    {
        return f > other.f;
    }
};

bool check(Node node) // 检查节点合法性
{
    if (node.x < 1 || node.x > 1000 || node.y < 1 || node.y > 1000) // 判断边界
        return false;
    return true;
}

int Euclidean(const Node &a, const Node &b) // 欧几里得距离
{
    return pow(a.x - b.x, 2) + pow(a.y - b.y, 2); // 统一不开根号,这样可以提高精度
}

void astar(const Node &start, const Node &end)
{
    priority_queue<Node> q; // 相当于open list
    q.push(start);
    while (!q.empty())
    {
        Node cur = q.top();
        q.pop();
        close[cur.x][cur.y] = true; // 加入close集

        if (cur.x == end.x && cur.y == end.y) // 到达目标节点
            break;

        for (int i = 0; i < 8; i++)
        {
            Node next = Node(cur.x + dir[i][0], cur.y + dir[i][1]);
            if (!check(next) || close[next.x][next.y]) // 越界和在close集的跳过
                continue;

            if (moves[next.x][next.y] == 0 || moves[cur.x][cur.y] + 1 < moves[next.x][next.y])
            {
                moves[next.x][next.y] = moves[cur.x][cur.y] + 1;
                next.g = cur.g + 5; // 统一不开根号,这样可以提高精度,马走日,1 * 1 + 2 * 2 = 5
                next.h = Euclidean(next, end);
                next.f = next.g + next.h;
                q.push(next); // 在开放集但更优的节点我也入队了,因为优先队列会把更小的节点弹出来,所以不影响
            }
        }
    }
}

int main()
{
    int n;
    cin >> n;
    int a1, a2, b1, b2; // 起点和终点坐标
    while (n--)
    {
        cin >> a1 >> a2 >> b1 >> b2;
        Node start = Node(a1, a2), end = Node(b1, b2);
        start.g = 0;
        start.h = Euclidean(start, end);
        start.f = start.g + start.h;

        memset(moves, 0, sizeof(moves));
        memset(close, 0, sizeof(close));
        astar(start, end);
        cout << moves[b1][b2] << endl;
    }
    return 0;
}
相关推荐
seabirdssss10 分钟前
力扣_876. 链表的中间结点
java·数据结构·算法·leetcode·链表
JhonKI22 分钟前
【算法】BFS解决最短路径问题
算法·php·宽度优先
加德霍克1 小时前
数据结构之算法
数据结构·学习·算法·排序算法
嗨信奥1 小时前
GESP C++等级考试 二级真题(2024年9月)
c++
GZ_TOGOGO1 小时前
【干货分享】Boosting算法简单案例
算法·机器学习·boosting
绵绵细雨中的乡音2 小时前
功能强大的stringstream类
c++·算法
秋凉 づᐇ2 小时前
数据结构(汇总)
java·数据结构·算法
灼华十一2 小时前
算法编程题-寻找最近的回文数
算法·leetcode·面试·golang
真理Eternal2 小时前
手搓人工智能-最优化算法(1)最速梯度下降法,及推导过程
人工智能·算法·机器学习
JXH_1232 小时前
ms-hot29 解码方法
c++·算法·leetcode