上一篇文章已经详细介绍了A*的算法思想,即靠实际代价G与预估代价H为待搜索节点划分搜索优先级,从而有向地遍历节点。
比如要在这么一张图中快速获取最短路径:
绿色:路径节点
灰色:关闭集合中的节点
蓝色:开放集合中的节点
1. 核心数据结构准备
重点内容:
- 节点(Node)类的定义
- 位置表示方法
- 优先级队列的选择
关键代码:
// 节点类定义
struct Node {
Vector2 position; // 位置坐标
Node* parent; // 父节点指针
float g; // 起点到当前节点的代价
float h; // 当前节点到终点的启发值
float f() const { return g + h; } // 综合评估值
};
// 位置结构体
struct Vector2 {
int x, y;
bool operator==(const Vector2& other) const {
return x == other.x && y == other.y;
}
};
2. 算法初始化
实现步骤:
- 创建开放列表(openList)和关闭列表(closedList)
- 将起点加入开放列表
- 设置起点的G值和H值
关键代码:
// 初始化开放列表(优先队列)
auto cmp = [](Node* a, Node* b) {
if(a->f() != b->f())
return a->f() > b->f();
return a->h > b->h;
};
priority_queue<Node*, vector<Node*>, decltype(cmp)> openList(cmp);
// 初始化起点节点
Node* startNode = new Node{startPos, nullptr, 0, heuristic(startPos, endPos)};
openList.push(startNode);
为什么这样排序?
- 保证最优性
- F = G + H 是A*算法的核心公式,确保在启发式函数可采纳(即H ≤ 真实代价)时能找到最短路径。
- 始终扩展F值最小的节点,相当于在"当前已知的最优路径"上推进。
- 引导搜索方向
- H值 作为"指南针",使搜索优先朝向目标方向。
- 当F值相同时,选择H值更小的节点能更快接近终点。
- 避免无效搜索**
- 若仅按G值排序(如Dijkstra),会盲目均匀扩散。
- 若仅按H值排序(如最佳优先搜索),可能找到非最优路径。
3. 主循环逻辑
实现步骤:
- 从开放列表取出F值最小的节点
- 检查是否到达终点
- 将当前节点加入关闭列表
- 遍历所有邻居节点
关键代码:
while (!openList.empty()) {
// 获取F值最小的节点
Node* current = openList.top();
openList.pop();
// 检查是否到达终点
if (current->position == endPos) {
return reconstructPath(current);
}
// 加入关闭列表
closedSet.insert(current->position);
// 遍历邻居节点
for (auto& neighborPos : getNeighbors(current->position)) {
// ...处理邻居节点
}
}
4. 邻居节点处理
实现步骤:
- 跳过关闭列表中的节点
- 跳过障碍物
- 计算新的路径代价
- 更新或添加节点到开放列表
关键代码:
// 跳过已处理的节点
if (closedSet.find(neighborPos) != closedSet.end())
continue;
// 跳过障碍物
if (isObstacle(neighborPos))
continue;
// 计算新G值
float newG = current->g + getMoveCost(current->position, neighborPos);
// 检查节点是否在开放列表中
if (openSet.find(neighborPos) == openSet.end() || newG < existingNode->g) {
// 创建或更新节点
Node* neighbor = new Node{
neighborPos,
current,
newG,
heuristic(neighborPos, endPos)
};
// 加入开放列表
openList.push(neighbor);//无需重新建堆,直接压入新的节点
openSet[neighborPos] = neighbor;
}
开放列表更新问题:修改节点G值后需重新调整堆结构(对优先队列重建堆),本文章的做法是直接加入新创建的节点。
5. 路径回溯
实现步骤:
- 从终点节点开始回溯
- 沿着父节点指针逆向收集路径点
- 反转路径顺序
关键代码:
vector<Vector2> reconstructPath(Node* endNode) {
vector<Vector2> path;
Node* current = endNode;
while (current != nullptr) {
path.push_back(current->position);
current = current->parent;
}
reverse(path.begin(), path.end());
return path;
}
6. 关键辅助函数实现
(1) 启发式函数
int Node::CalculateHCost(const Node& end) {//计算预估代价H
return heuristic(position, end.position);//采用曼哈顿距离
}
int Node::CalculateGCost(const Node& end) {//计算实际代价G
return euclidean(position, end.position);//采用对角线距离
}
如上所示,对H(启发式)使用曼哈顿距离,而对G(实际代价)使用欧式距离,采用了混合策略设计
H用曼哈顿距离的原因
- 可采纳性保证:曼哈顿距离在网格中永远 ≤ 真实代价(满足A*的最优解条件)。
- 例如:在2D网格中,真实对角线距离是√2≈1.414,而曼哈顿距离是1+1=2(高估了,但若用对角线距离则需调整)
- 计算效率:仅需加减法,比欧式距离的平方和开方快10倍以上。
(2) 移动代价计算
// 曼哈顿距离
float heuristic(Vector2 a, Vector2 b) {
return 10 * abs(a.x - b.x) + abs(a.y - b.y);
}
// 欧几里得距离
float euclidean(Vector2 a, Vector2 b) {
int dx = abs(a.x - b.x);
int dy = abs(a.y - b.y);
return 10 * sqrt(dx*dx + dy*dy); // 精确但慢
}
// 对角线距离
float diagonalHeuristic(Vector2 a, Vector2 b) {
int dx = abs(a.x - b.x);
int dy = abs(a.y - b.y);
return 10 * (dx + dy) + 6 * min(dx, dy);//水平移动代价为10,沿对角线移动代价为20
}
选择建议
- 精度:欧式距离 > 对角线距离
- 性能:对角线距离 > 欧式距离(快3-5倍)
- 推荐:在网格游戏中使用对角线距离,在物理仿真中使用欧式距离。二者的选择本质是精度与性能的权衡。
(3) 邻居节点获取
vector<Vector2> getNeighbors(Vector2 pos) {
vector<Vector2> neighbors;
// 八方向移动
const vector<Vector2> directions = = {
{1,0}, {0,1}, {-1,0}, {0,-1}, // 四方向
{1,1}, {1,-1}, {-1,1}, {-1,-1} // 对角线
};
for (auto& dir : directions) {
Vector2 neighbor = {pos.x + dir.x, pos.y + dir.y};
neighbors.push_back(neighbor);
}
return neighbors;
}
7. 完整流程整合
伪代码:
function findPath(start, end):
initOpenList(start)
initClosedList()
while openList not empty:
current = popMinFNode()
if current == end: return buildPath(current)
markAsClosed(current)
for each neighbor of current:
if inClosed(neighbor) or isObstacle(neighbor): skip
newG = calculateNewCost(current, neighbor)
if neighbor not in openList or newG < oldG:
updateOrAddNode(neighbor, current, newG)
return emptyPath // 未找到路径
graph TD
A[初始化起点] --> B{开放列表空?}
B -->|是| C[寻路失败]
B -->|否| D[取F最小节点]
D --> E{是终点?}
E -->|是| F[回溯路径]
E -->|否| G[加入关闭集]
G --> H[遍历邻居]
H --> I{可访问?}
I -->|是| J{需更新?}
J -->|是| K[更新节点]
J -->|否| H
K --> H
效果: