3、静态路径规划算法(A*):A*算法的原理、启发式函数设计、Python实现与可视化

好,咱们进入正题。静态路径规划,说白了就是在已知地图上找一条最短路径。你想想看,机器人要在一个完全已知的环境里从A点走到B点,怎么走最省事?这就是A*算法要干的事。

我个人习惯把A*看作是Dijkstra的升级版。Dijkstra像个无头苍蝇一样四面八方乱撞,而A*则像个老司机,知道往哪个方向开更有可能到达目的地。这个「知道往哪个方向开」的能力,就是靠启发式函数实现的。

3.1 A*算法的核心原理

A*的核心公式其实特别简单:

f(n) = g(n) + h(n)

我来解释一下这三个参数:

  • g(n):从起点到当前节点n的实际代价。说白了就是已经走了多远。
  • h(n):从当前节点n到终点的估计代价。这是启发式函数,是A*的灵魂。
  • f(n):总代价,也就是g(n)和h(n)的和。

算法每次从待选节点中挑f(n)最小的那个去扩展。嗯,这里要注意,如果h(n)始终为0,A*就退化成了Dijkstra。我在项目中遇到过有人把h(n)设得太大,结果算法跑得飞快但路径不是最优的——这就是所谓的「不可采纳」了。

关键点:A*算法保证找到最优路径的条件是:启发式函数h(n)必须是「可采纳的」,即h(n) ≤ 从n到终点的真实代价。说白了就是不能高估距离。

3.2 启发式函数设计

启发式函数怎么设计?这得看你的地图是什么类型的。我总结了几种常见情况:

地图类型 推荐启发式函数 说明
网格地图(四方向移动) 曼哈顿距离 |x1-x2| + |y1-y2|
网格地图(八方向移动) 切比雪夫距离 max(|x1-x2|, |y1-y2|)
任意方向移动 欧几里得距离 √((x1-x2)² + (y1-y2)²)
路网/图结构 直线距离 两点间直线距离

我曾经在一个仓储机器人项目里吃过亏。当时用的是曼哈顿距离,但机器人实际上可以斜着走。结果路径规划出来总是绕远路,后来改成切比雪夫距离,效果立竿见影。你想想看,启发式函数选错了,算法再牛也白搭。

小技巧:如果你不确定用哪种距离,可以试试「加权A*」。把h(n)乘以一个系数w(比如1.2),算法会更快但可能不是最优。这在实时性要求高的场景下很实用。

3.3 Python实现

好,咱们直接上代码。我习惯用面向对象的方式写,清晰好维护。

import heapq
import math
import matplotlib.pyplot as plt
import numpy as np

class AStar:
    def __init__(self, grid, start, goal):
        self.grid = grid  # 0表示可通行,1表示障碍物
        self.start = start
        self.goal = goal
        self.rows = len(grid)
        self.cols = len(grid[0])
    
    def heuristic(self, a, b):
        """使用欧几里得距离作为启发式函数"""
        return math.sqrt((a[0] - b[0])**2 + (a[1] - b[1])**2)
    
    def get_neighbors(self, node):
        """获取四方向邻居节点"""
        neighbors = []
        for dx, dy in [(-1,0), (1,0), (0,-1), (0,1)]:
            nx, ny = node[0] + dx, node[1] + dy
            if 0 <= nx < self.rows and 0 <= ny < self.cols:
                if self.grid[nx][ny] == 0:  # 可通行
                    neighbors.append((nx, ny))
        return neighbors
    
    def search(self):
        """A*主搜索逻辑"""
        # 优先队列:存储 (f值, g值, 节点)
        open_set = []
        heapq.heappush(open_set, (0, 0, self.start))
        
        # 记录从哪里来的
        came_from = {}
        # g值表
        g_score = {self.start: 0}
        # f值表
        f_score = {self.start: self.heuristic(self.start, self.goal)}
        
        while open_set:
            _, current_g, current = heapq.heappop(open_set)
            
            if current == self.goal:
                # 找到路径,回溯
                path = []
                while current in came_from:
                    path.append(current)
                    current = came_from[current]
                path.append(self.start)
                path.reverse()
                return path
            
            for neighbor in self.get_neighbors(current):
                tentative_g = current_g + 1  # 每步代价为1
                
                if neighbor not in g_score or tentative_g < g_score[neighbor]:
                    came_from[neighbor] = current
                    g_score[neighbor] = tentative_g
                    f = tentative_g + self.heuristic(neighbor, self.goal)
                    f_score[neighbor] = f
                    heapq.heappush(open_set, (f, tentative_g, neighbor))
        
        return None  # 没找到路径

这段代码我用了好几年,基本没大改过。你注意看第38行,我用了欧几里得距离。如果换成曼哈顿距离,把heuristic函数改一下就行。

注意:代码里我用了四方向移动。如果你需要八方向,get_neighbors方法要加上对角线方向,同时代价也要改成√2。我曾经有个学生没改代价,结果路径看起来歪歪扭扭的。

3.4 可视化展示

光有代码不够,咱们得看到效果。下面这个可视化函数,我建议你直接拿去用:

def visualize_path(grid, path, start, goal):
    """可视化路径规划结果"""
    grid_vis = np.array(grid, dtype=float)
    
    # 标记路径
    if path:
        for node in path:
            grid_vis[node[0]][node[1]] = 0.5  # 灰色表示路径
    
    # 标记起点和终点
    grid_vis[start[0]][start[1]] = 0.8  # 浅色
    grid_vis[goal[0]][goal[1]] = 0.9    # 更浅
    
    plt.figure(figsize=(8, 8))
    plt.imshow(grid_vis, cmap='viridis', interpolation='nearest')
    plt.colorbar(label='可通行性')
    plt.title('A*路径规划结果')
    plt.grid(True, alpha=0.3)
    
    # 标注起点和终点
    plt.text(start[1], start[0], 'S', ha='center', va='center', fontsize=12, color='white')
    plt.text(goal[1], goal[0], 'G', ha='center', va='center', fontsize=12, color='white')
    
    plt.show()

# 使用示例
if __name__ == "__main__":
    # 创建一个10x10的地图,中间有障碍物
    grid = [[0]*10 for _ in range(10)]
    for i in range(3, 7):
        grid[i][5] = 1  # 竖着的障碍物
    
    start = (0, 0)
    goal = (9, 9)
    
    astar = AStar(grid, start, goal)
    path = astar.search()
    
    if path:
        print(f"找到路径,共{len(path)}步")
        visualize_path(grid, path, start, goal)
    else:
        print("无法到达目标点")

运行这段代码,你会看到一条从左上角到右下角的路径,绕过了中间的障碍物。嗯,这就是A*的威力。

3.5 避坑指南

最后,我分享几个实战中踩过的坑:

  • 启发式函数不可采纳:我曾经把h(n)设成欧几里得距离的2倍,结果路径不是最优的。后来改成1倍就好了。
  • 地图太大导致内存爆炸:1000x1000的地图,open_set里可能存几十万个节点。我建议用双向A*或者跳点搜索(JPS)来优化。
  • 动态障碍物:A*是静态算法,如果障碍物会动,你得用D* Lite或者RRT*。这个咱们后面章节会讲。

我的建议:刚开始学A*,先用小地图(10x10)调试。等逻辑跑通了,再慢慢放大。别一上来就搞1000x1000,debug起来你会崩溃的。

好了,A*的核心内容就这些。说白了就是三个东西:f=g+h的公式、合适的启发式函数、以及优先队列的实现。你把这个吃透了,后面学D*、RRT*都会轻松很多。