A*(A-star)路径搜索算法是一种广泛应用于图搜索和路径规划中的启发式算法。它用于找到从起点到目标节点的最优路径,常用于机器人导航、游戏AI以及路径规划等领域。A算法结合了Dijkstra算法的优点和贪心最佳优先搜索的启发式方法,使其在效率和效果上表现优异。下面是对A算法的详细介绍:
一、算法原理
A*算法使用一个评价函数f(n)
来指导搜索过程,其中f(n)
是以下两个函数的和:
g(n)
: 从起点到节点n
的实际代价(通常是距离或消耗)。h(n)
: 从节点n
到目标节点的估计代价(启发式函数)。- 因此,
f(n) = g(n) + h(n)
。
启发式函数h(n)
是启发式估计,从当前节点到目标节点的代价。为了确保A*算法的最优性,启发式函数必须是可接受的(admissible),即它从不高估实际代价。常见的启发式函数包括:
- 曼哈顿距离(用于格子地图)
- 欧几里得距离
- 对角线距离
二、算法步骤
初始化:
- 创建一个开放列表(open list),初始时仅包含起点节点。
- 创建一个封闭列表(closed list),初始为空。
- 设置起点节点的
g值
为0,h值
为从起点到目标节点的启发式估计,f值
为g值 + h值
。搜索循环:
- 从开放列表中取出
f值
最小的节点作为当前节点n
。- 如果当前节点是目标节点,则搜索结束,成功找到路径。
- 否则,将当前节点
n
从开放列表移到封闭列表。- 对于当前节点
n
的每个相邻节点m
:
- 如果相邻节点
m
在封闭列表中,跳过。- 如果相邻节点
m
不在开放列表中:
- 将
m
添加到开放列表。- 计算
m
的g值
(从起点到m
的实际代价)、h值
(从m
到目标节点的启发式估计)和f值
。- 设置相邻节点
m
的父节点为当前节点n
。- 如果相邻节点
m
已经在开放列表中,检查新的路径是否更优:
- 如果新的路径
g值
更小,更新m
的g值
、f值
和父节点。路径重构:
- 如果找到目标节点,从目标节点开始,通过父节点逐个回溯到起点,得到完整路径。
三、C++代码
代码是自己写了个初稿,借助ChatGPT优化后的,配置好VSCode环境可以直接跑。
Windows下的VSCode配置C++编译环境参考:Windows中VSCode配置编译C++环境与使用gdb调试-CSDN博客。
#include <iostream>
#include <vector>
#include <queue>
#include <cmath>
#include <unordered_set>
#include <memory>
using namespace std;
const int MAP_W = 8;
const int MAP_H = 8;
const int WEIGHT_STRAIGHT = 10;
const int WEIGHT_DIAGONAL = 14;
const char STATE_NONE = '0'; // 未被访问的可达节点
const char STATE_BARRIER = '1'; // 障碍
const char STATE_OPEN = '2'; // 开放列表中
const char STATE_CLOSE = '3'; // 关闭列表中
vector<vector<char>> MAP(MAP_H, vector<char>(MAP_W, STATE_NONE));
vector<vector<char>> stateList = MAP;
vector<pair<int, int>> barrierList = {
{2, 1}, {3, 1}, {4, 1}, {5, 1},
{0, 3}, {1, 3}, {2, 3}, {3, 3},
{3, 5}, {4, 5}, {5, 5}, {6, 5},
{6, 0}, {6, 1}, {6, 2}, {6, 3}, {6, 4}
};
void init() {
for (const auto& barrier : barrierList) {
MAP[barrier.first][barrier.second] = STATE_BARRIER;
stateList[barrier.first][barrier.second] = STATE_BARRIER;
}
}
struct Node {
int x, y;
int g; // 耗费值
int f; // 预测值
shared_ptr<Node> parent; // 父节点
Node(int x, int y, int endX, int endY, int g, shared_ptr<Node> parent)
: x(x), y(y), g(g), parent(parent) {
int dx = abs(endX - x);
int dy = abs(endY - y);
f = g + (dx + dy) * WEIGHT_STRAIGHT; // 曼哈顿距离
}
bool operator==(const Node& other) const {
return x == other.x && y == other.y;
}
struct HashFunction {
size_t operator()(const shared_ptr<Node>& node) const {
return hash<int>()(node->x) ^ hash<int>()(node->y);
}
};
};
struct Compare {
bool operator()(const shared_ptr<Node>& a, const shared_ptr<Node>& b) const {
return a->f > b->f;
}
};
priority_queue<shared_ptr<Node>, vector<shared_ptr<Node>>, Compare> openList;
unordered_set<shared_ptr<Node>, Node::HashFunction> closedList;
bool isValidPoint(int x, int y) {
return x >= 0 && y >= 0 && x < MAP_W && y < MAP_H;
}
void openNode(shared_ptr<Node> current, int endX, int endY) {
int directions[8][2] = {
{-1, 0}, {1, 0}, {0, -1}, {0, 1},
{1, 1}, {-1, 1}, {-1, -1}, {1, -1}
};
for (int i = 0; i < 8; ++i) {
int newX = current->x + directions[i][0];
int newY = current->y + directions[i][1];
if (!isValidPoint(newX, newY))
continue;
int cost = (i < 4) ? WEIGHT_STRAIGHT : WEIGHT_DIAGONAL;
if (stateList[newX][newY] == STATE_NONE || stateList[newX][newY] == STATE_OPEN) {
auto newNode = make_shared<Node>(newX, newY, endX, endY, current->g + cost, current);
openList.push(newNode);
stateList[newX][newY] = STATE_OPEN;
}
}
}
vector<shared_ptr<Node>> searchPath(int startX, int startY, int endX, int endY) {
vector<shared_ptr<Node>> path;
auto startNode = make_shared<Node>(startX, startY, endX, endY, 0, nullptr);
openList.push(startNode);
closedList.insert(startNode);
while (!openList.empty()) {
auto current = openList.top();
openList.pop();
if (current->x == endX && current->y == endY) {
while (current) {
path.push_back(current);
current = current->parent;
}
break;
}
stateList[current->x][current->y] = STATE_CLOSE;
closedList.insert(current);
openNode(current, endX, endY);
}
return path;
}
void printMap() {
cout << "\nMAP:\n";
for (const auto& row : MAP) {
for (const auto& cell : row) {
cout << cell;
}
cout << endl;
}
}
void printStateList() {
cout << "\nStateList:\n";
for (const auto& row : stateList) {
for (const auto& cell : row) {
cout << cell;
}
cout << endl;
}
}
int main() {
int startX = 5, startY = 0;
int endX = 7, endY = 1;
init();
vector<shared_ptr<Node>> path = searchPath(startX, startY, endX, endY);
for (auto node : path) {
MAP[node->x][node->y] = '#';
}
printMap();
printStateList();
if (path.empty()) {
cout << "Unable to reach the target point!!!\n";
} else {
cout << "Path found!!!\n";
}
return 0;
}
四、优点与缺点
优点
- 高效性:A*算法通过结合实际代价和启发式估计,能够快速找到最优路径。
- 灵活性:适用于多种图结构和启发式函数。
- 最优性:如果启发式函数是可接受的,A*算法保证找到最优路径。
缺点
- 内存消耗:A*算法需要维护开放列表和封闭列表,可能占用大量内存,特别是在图非常大的情况下。
- 计算量:在最坏情况下,A*算法可能需要遍历大量节点,计算复杂度较高。
五、应用场景
- 机器人导航:机器人在地图中规划路径,避开障碍物到达目标位置。
- 游戏AI:游戏中的角色在地图中寻找最短路径,例如从一个位置到另一个位置。
- 路径规划:应用于物流、交通系统等领域,优化运输路径。
六、Dijkstra算法与A*算法对比
Dijkstra算法和A*算法都是用于图搜索和路径规划的经典算法。它们之间有一些相似之处,但也有显著的区别。以下是对这两种算法的比较:
1. 基本原理
Dijkstra算法
- 原理:Dijkstra算法是一种无权图或正权图中的单源最短路径算法。它逐步扩展起点到所有其他节点的最短路径,直到找到目标节点或所有节点都已访问。
- 成本函数:Dijkstra算法只考虑从起点到当前节点的实际代价
g(n)
。 - 特点:不需要启发式函数,因此适用于没有明确目标或所有目标具有相同优先级的情况。
A*算法
- 原理:A*算法是在Dijkstra算法的基础上加入了启发式函数
h(n)
,用于估计从当前节点到目标节点的代价。它结合了实际代价和启发式估计来指导搜索过程。 - 成本函数:A*算法考虑从起点到当前节点的实际代价
g(n)
以及从当前节点到目标节点的启发式估计h(n)
,即f(n) = g(n) + h(n)
。 - 特点:需要启发式函数,因此适用于有明确目标并且可以估计目标距离的情况。
2. 性能和效率
Dijkstra算法
- 时间复杂度:O((V + E)logV),其中V是节点数,E是边数。在稠密图中表现较差。
- 空间复杂度:O(V),需要存储所有节点的距离和访问状态。
- 搜索范围:Dijkstra算法会探索所有可能的路径,直到找到目标节点的最短路径。因此,它在没有启发式信息的情况下可能会比较慢。
A*算法
- 时间复杂度:取决于启发式函数的好坏。在最坏情况下,复杂度与Dijkstra算法相同,但在实际应用中通常比Dijkstra算法更快。
- 空间复杂度:O(V),需要存储所有节点的距离、启发式估计和访问状态。
- 搜索范围:A*算法利用启发式函数引导搜索,通常会探索较少的节点,特别是在启发式函数较好的情况下。
3. 应用场景
Dijkstra算法
- 适用情况:适用于所有边权为非负的图,特别是没有明确目标或所有目标优先级相同的情况。
- 常见应用:网络路由、交通规划、资源分配等。
A*算法
- 适用情况:适用于需要寻找特定目标节点的最短路径,并且可以定义合理启发式函数的情况。
- 常见应用:游戏AI路径规划、机器人导航、地图路径规划等。
4. 优缺点
Dijkstra算法
优点:
- 确保找到从起点到所有节点的最短路径。
- 不需要启发式函数,适用范围广。
缺点:
- 对于大规模图搜索效率较低,因为它没有利用任何启发信息。
- 在没有明确目标节点时,搜索范围可能非常大。
A*算法
优点:
- 通过启发式函数引导搜索,通常比Dijkstra算法更快。
- 启发式函数设计合理时,能够高效找到最优路径。
缺点:
- 需要合理的启发式函数设计,不同问题需要不同的启发式函数。
- 启发式函数不合理时,可能退化为Dijkstra算法,效率降低。
5. 示例对比
Dijkstra算法示例
在一个简单的无权图中,Dijkstra算法会逐步扩展起点的所有邻居,直到找到目标节点。例如,在一个5x5的网格中从左上角到右下角的路径,Dijkstra算法会访问所有可能的路径,直到找到最短路径。
A*算法示例
在同样的5x5网格中,A算法会根据启发式函数(例如曼哈顿距离)引导搜索过程,通常只会访问较少的节点。例如,如果起点在左上角,目标在右下角,A算法会优先考虑向右和向下的路径,因为这些路径在启发式估计中距离目标较近。
总结
- Dijkstra算法:适用于所有边权为非负的图,适合没有明确目标或所有目标具有相同优先级的情况。它的搜索范围较大,但能够确保找到从起点到所有节点的最短路径。
- A*算法:适用于需要寻找特定目标节点的最短路径,并且可以定义合理启发式函数的情况。它的搜索范围较小,效率更高,特别是在启发式函数设计合理的情况下。