概述
A*(A-star)算法是一种在图中寻找从初始节点到目标节点最短路径的启发式搜索算法。它结合了Dijkstra算法的确保性(保证找到一条最短路径)和贪心算法的高效性(快速找到目标)。A*算法通过评估函数f(n) = g(n) + h(n)
来工作,其中g(n)
是从起始点到任何顶点n的实际成本,而h(n)
是从顶点n到目标的估计最低成本,通常用启发式函数来计算,这个函数需要事先设计来反映实际的地形或环境特征。
A*算法具有以下显著特性:
- 最优性:当启发式函数
h(n)
满足某些条件时,A*算法能保证找到一条最低成本路径。 - 效率: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*算法的步骤可以概括如下:
- 初始化 :
- 创建两个集合:开放集(Open Set)和关闭集(Closed Set)。
- 将起始节点加入开放集,并将起始节点的
g(n)
值设为0,h(n)
值根据启发式函数计算。 - 将起始节点的父节点设为null。
- 循环执行以下步骤,直到开放集为空或找到目标节点 :
- 选择节点 :从开放集中选择具有最低
f(n)
值的节点,即f(n) = g(n) + h(n)
。 - 检查目标:如果选择的节点就是目标节点,则路径已找到,算法结束。
- 移动节点:将选择的节点从开放集移至关闭集。
- 扩展节点 :对于选择的节点的每一个邻居节点:
- 如果邻居节点不在关闭集中,计算它的
g(n)
值(从起始节点到邻居节点的成本),并计算f(n)
值。 - 如果邻居节点不在开放集中,将其添加到开放集中,并记录当前节点为其父节点。
- 如果邻居节点已在开放集中,但通过当前节点到达它的
g(n)
值更低,则更新它的g(n)
值和父节点。
- 如果邻居节点不在关闭集中,计算它的
- 更新启发式函数 :对于每个新加入开放集的节点或更新了
g(n)
值的节点,重新计算它的f(n)
值。
- 选择节点 :从开放集中选择具有最低
- 路径重建 :
- 一旦找到目标节点,从目标节点开始,通过其父节点回溯到起始节点,构建出完整的路径。
- 输出路径 :
- 输出从起始节点到目标节点的路径。
代码实现
题目描述
在象棋中,马和象的移动规则分别是"马走日"和"象走田"。现给定骑士的起始坐标和目标坐标,要求根据骑士的移动规则,计算从起点到达目标点所需的最短步数。
棋盘大小 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;
}