第4章:启发式搜索——A*算法核心思想与实战

各位同学,欢迎来到航迹规划的核心章节。今天我们要聊的A*算法,可以说是路径搜索领域的“万金油”。我在做机器人导航项目时,80%的场景第一反应就是上A*。为什么?因为它够聪明,够快,而且实现起来不复杂。

4.1 从Dijkstra到A*:我们为什么要“启发”

先回忆一下Dijkstra算法。它像个勤奋的扫地机器人,把每个角落都扫一遍,保证找到最短路径。但问题来了——它太“老实”了。明明目标在东北方向,它还要往西南方向探索一圈。

我当年第一次用Dijkstra做AGV调度时,地图稍微大一点,计算时间就飙到秒级。老板在旁边看着,我那个汗啊……

A*算法就是在Dijkstra的基础上,加了一个“方向感”。它用启发函数来估计当前点到目标点的距离,优先探索那些“看起来有希望”的节点。说白了,就是给搜索过程装了个指南针。

4.2 A*核心思想:f = g + h

A*的核心公式就一个:

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

解释一下:

  • g(n):从起点到当前节点n的实际代价。这个我们已经走过了,是确定值。
  • h(n):从当前节点n到目标节点的估计代价。这是启发函数,是估计值。
  • f(n):总代价,也就是我们用来排序的依据。

算法每次从open list中取出f值最小的节点进行扩展。嗯,这里要注意:h(n)必须满足可采纳性——它不能高估实际代价。否则A*就退化成不保证最优的贪心算法了。

核心要点:h(n)估计得越准,搜索效率越高;h(n)=0时,A*退化为Dijkstra。

4.3 启发函数设计:三种距离的抉择

启发函数怎么选?这取决于你的地图类型。我整理了一张表,方便你对照:

距离类型 计算公式 适用场景 特点
曼哈顿距离 |x1-x2| + |y1-y2| 四方向网格地图 计算快,但可能低估
欧氏距离 √((x1-x2)² + (y1-y2)²) 八方向或自由移动 最精确,但有开方运算
切比雪夫距离 max(|x1-x2|, |y1-y2|) 八方向网格(国王移动) 兼顾精度和速度

我在做仓储机器人项目时,一开始用了曼哈顿距离。结果发现机器人明明可以斜着走,但路径规划出来全是直角转弯,效率低了不少。后来换成切比雪夫距离,路径就顺滑多了。

我的建议:如果地图允许八方向移动,优先用切比雪夫距离。它比欧氏距离快(没有开方),比曼哈顿距离准。

4.4 A*算法代码实现(Python)

光说不练假把式。下面是我常用的A*实现模板,你直接拿去改改就能用:

import heapq

def astar(start, goal, grid):
    # 方向向量:四方向或八方向
    # 这里用四方向为例
    directions = [(0,1), (0,-1), (1,0), (-1,0)]
    
    open_list = []
    heapq.heappush(open_list, (0, start))
    
    g_score = {start: 0}
    f_score = {start: heuristic(start, goal)}
    came_from = {}
    
    while open_list:
        current = heapq.heappop(open_list)[1]
        
        if current == goal:
            return reconstruct_path(came_from, current)
        
        for dx, dy in directions:
            neighbor = (current[0] + dx, current[1] + dy)
            
            # 检查边界和障碍物
            if not in_bounds(neighbor, grid) or grid[neighbor[0]][neighbor[1]] == 1:
                continue
            
            tentative_g = g_score[current] + 1  # 假设每步代价为1
            
            if neighbor not in g_score or tentative_g < g_score[neighbor]:
                came_from[neighbor] = current
                g_score[neighbor] = tentative_g
                f_score[neighbor] = tentative_g + heuristic(neighbor, goal)
                heapq.heappush(open_list, (f_score[neighbor], neighbor))
    
    return None  # 无路径

def heuristic(a, b):
    # 曼哈顿距离
    return abs(a[0] - b[0]) + abs(a[1] - b[1])

我曾经踩过的坑:open list用list+sort()而不是heapq。地图一大,排序开销直接让算法慢10倍。记住,优先队列是A*的标配

4.5 优化技巧:让A*跑得更快

实际项目中,原始A*往往不够用。这里分享几个我常用的优化手段:

  1. 双向A*:从起点和终点同时搜索,相遇即停止。能减少约50%的探索节点。
  2. JPS(跳点搜索):在网格地图中,跳过那些“明显不需要探索”的节点。我做过测试,比普通A*快10倍以上。
  3. 权重启发式:把f = g + h改成f = g + w*h,其中w>1。虽然不保证最优,但搜索速度大幅提升。适合实时性要求高的场景。
  4. 分层规划:先在高精度地图上粗规划,再在局部细化。我做过一个城市级路径规划,用这招把计算时间从分钟级降到了秒级。

4.6 知识体系总览

下面这张图,是我梳理的本章知识结构。你把它存下来,复习时对着看:

A*算法 核心思想 f = g + h 启发函数设计 曼哈顿/欧氏/切比雪夫 代码实现 Python + heapq g(n): 实际代价 h(n): 估计代价 曼哈顿距离 欧氏距离 切比雪夫距离 双向A* / JPS / 权重启发 核心:h(n)可采纳 + 优先队列 + 场景适配 启发函数决定搜索效率,代码实现决定运行速度

你看,A*的核心其实就三块:公式理解、启发函数选择、代码实现。把这三点吃透了,大部分路径规划问题都能搞定。

实战建议:刚开始学A*时,别急着上优化技巧。先把基础版本跑通,用不同启发函数对比效果。我当年就是先跑通基础版,再一步步加JPS、双向搜索,这样理解才扎实。

好了,这一章的内容就到这里。A*算法说难不难,说简单也不简单。关键是多动手、多调试。下次遇到路径规划问题,不妨先问问自己:我的启发函数选对了吗?

专注资料整理