4. 全局路径规划:Dijkstra算法、A*算法原理与实现、栅格地图构建

说到全局路径规划,我脑子里第一个蹦出来的场景,就是几年前做仓储机器人项目时的那个下午。机器人明明知道目标在哪,却总在货架中间绕远路。说白了,全局路径规划就是给机器人画一张“大地图”,然后告诉它:从A到B,哪条路最划算。

这一章,我们重点啃两个经典算法——Dijkstra和A*。它们就像路径规划界的“倚天剑”和“屠龙刀”。我会结合栅格地图,把原理、实现、还有我踩过的坑,一次性讲清楚。

4.1 栅格地图:让机器人看懂世界

机器人不是人,它看不懂“前面第三个路口左转”。它需要一种结构化的表达方式。栅格地图,就是最直观的一种。

简单说,我们把环境切成一个个小格子。每个格子要么是“空地”(可通行),要么是“障碍物”(不可通行)。就像围棋棋盘,黑子是障碍,白子是路。

核心概念: 栅格地图 = 二维数组。0表示空闲,1表示障碍。分辨率决定了地图的精细程度。

我习惯用Python的numpy来构建栅格地图。举个例子:

import numpy as np

# 创建一个10x10的栅格地图,全部初始化为0(空闲)
grid_map = np.zeros((10, 10), dtype=int)

# 手动添加一些障碍物(1表示障碍)
grid_map[2, 3:7] = 1   # 第3行,第4到7列是墙
grid_map[5, 1:9] = 1   # 第6行,第2到9列是墙
grid_map[7, 4] = 1     # 一个孤立的障碍物

print("栅格地图:")
print(grid_map)

嗯,这里要注意:栅格的分辨率是个双刃剑。分辨率太高,计算量爆炸;分辨率太低,机器人可能钻不过窄通道。我在项目中一般取0.05米到0.1米,具体看机器人尺寸。

我的经验: 建图时,记得给机器人留一个“膨胀层”。就是把障碍物周围再扩大一圈,防止机器人蹭到墙。我曾经因为没做膨胀,机器人卡在门框里,场面一度很尴尬。

4.2 Dijkstra算法:稳扎稳打的“地毯式搜索”

Dijkstra算法,说白了就是“从起点开始,一层一层往外扩”。它保证找到的路径是全局最短的,但代价是——慢。

你想想看,它就像个强迫症患者,必须把所有可能的路都走一遍,才敢告诉你哪条最近。

4.2.1 算法原理

核心思想就一句话:维护一个“未访问节点”的集合,每次从中选出距离起点最近的那个,然后更新它邻居的距离。

具体步骤:

  1. 初始化:起点距离为0,其他节点距离为无穷大。
  2. 循环:从未访问节点中,选出距离最小的节点u。
  3. 更新:对于u的所有邻居v,如果通过u到v的距离更短,就更新v的距离。
  4. 标记u为已访问。
  5. 重复,直到所有节点都被访问,或者到达目标点。
注意: Dijkstra不能处理负权边。好在栅格地图里,每条边的代价都是正数(比如移动一步代价为1),所以完全没问题。

4.2.2 代码实现

我直接给你一个在栅格地图上跑Dijkstra的简化版代码。核心逻辑都在里面了。

import heapq
import numpy as np

def dijkstra(grid, start, goal):
    """
    grid: 二维栅格地图,0可通行,1障碍
    start: 起点坐标 (x, y)
    goal: 终点坐标 (x, y)
    返回: 路径列表,从起点到终点
    """
    rows, cols = grid.shape
    # 距离数组,初始化为无穷大
    dist = np.full((rows, cols), np.inf)
    dist[start] = 0
    
    # 前驱节点,用于回溯路径
    parent = {}
    
    # 优先队列:(距离, 坐标)
    pq = [(0, start)]
    
    # 四个方向移动:上、下、左、右
    directions = [(-1,0), (1,0), (0,-1), (0,1)]
    
    while pq:
        current_dist, current = heapq.heappop(pq)
        
        # 如果当前距离大于记录的距离,跳过(过时数据)
        if current_dist > dist[current]:
            continue
        
        # 到达目标,结束
        if current == goal:
            break
        
        # 遍历四个邻居
        for dx, dy in directions:
            nx, ny = current[0] + dx, current[1] + dy
            
            # 边界检查
            if 0 <= nx < rows and 0 <= ny < cols:
                # 障碍物检查
                if grid[nx, ny] == 1:
                    continue
                
                # 新距离 = 当前距离 + 1(每一步代价为1)
                new_dist = current_dist + 1
                
                # 如果新距离更短,更新
                if new_dist < dist[nx, ny]:
                    dist[nx, ny] = new_dist
                    parent[(nx, ny)] = current
                    heapq.heappush(pq, (new_dist, (nx, ny)))
    
    # 回溯路径
    path = []
    if goal in parent or goal == start:
        node = goal
        while node != start:
            path.append(node)
            node = parent[node]
        path.append(start)
        path.reverse()
    
    return path

# 使用示例
grid = np.zeros((5, 5), dtype=int)
grid[1, 1:4] = 1  # 障碍物
start = (0, 0)
goal = (4, 4)
path = dijkstra(grid, start, goal)
print("Dijkstra找到的路径:", path)

这段代码我用了heapq,也就是优先队列。每次取最小距离节点的时间复杂度是O(log n),比暴力遍历快得多。

4.3 A*算法:带“指南针”的搜索

Dijkstra太老实了,它不管目标在哪,都一圈一圈往外扩。A*就不一样了,它有个“指南针”——启发式函数。它会朝着目标方向优先搜索,所以通常比Dijkstra快得多。

我个人觉得,A*是工程中最实用的全局路径规划算法。没有之一。

4.3.1 算法原理

A*的核心公式就一个:

f(n) = g(n) + h(n)
  • g(n):从起点到当前节点n的实际代价。
  • h(n):从当前节点n到目标点的估计代价(启发式函数)。
  • f(n):综合代价,A*优先选择f(n)最小的节点。

说白了,g(n)是“已经走过的路”,h(n)是“预估还要走的路”。两者相加,就是总代价的估计。

启发式函数h(n)的选择很关键。最常用的是曼哈顿距离和欧几里得距离。

启发式函数 公式 特点
曼哈顿距离 |x1-x2| + |y1-y2| 只允许上下左右移动时,是最优的
欧几里得距离 sqrt((x1-x2)^2 + (y1-y2)^2) 允许任意方向移动时更准确
切比雪夫距离 max(|x1-x2|, |y1-y2|) 允许八方向移动时使用
避坑指南: 我曾经在项目中用了欧几里得距离,结果A*搜索效率反而比曼哈顿距离低。为什么?因为欧几里得距离有开方运算,计算量大。而且对于栅格地图(只能上下左右移动),曼哈顿距离更准确。所以,启发式函数的选择要和实际运动模型匹配。

4.3.2 代码实现

A*的代码和Dijkstra很像,区别就在于多了个启发式函数。你看:

import heapq
import numpy as np

def heuristic(a, b):
    """曼哈顿距离作为启发式函数"""
    return abs(a[0] - b[0]) + abs(a[1] - b[1])

def a_star(grid, start, goal):
    rows, cols = grid.shape
    
    # g(n) 和 f(n) 数组
    g_score = np.full((rows, cols), np.inf)
    g_score[start] = 0
    f_score = np.full((rows, cols), np.inf)
    f_score[start] = heuristic(start, goal)
    
    parent = {}
    
    # 优先队列:(f(n), 坐标)
    pq = [(f_score[start], start)]
    
    directions = [(-1,0), (1,0), (0,-1), (0,1)]
    
    while pq:
        _, current = heapq.heappop(pq)
        
        if current == goal:
            break
        
        for dx, dy in directions:
            nx, ny = current[0] + dx, current[1] + dy
            
            if 0 <= nx < rows and 0 <= ny < cols and grid[nx, ny] == 0:
                tentative_g = g_score[current] + 1
                
                if tentative_g < g_score[nx, ny]:
                    parent[(nx, ny)] = current
                    g_score[nx, ny] = tentative_g
                    f_score[nx, ny] = tentative_g + heuristic((nx, ny), goal)
                    heapq.heappush(pq, (f_score[nx, ny], (nx, ny)))
    
    # 回溯路径
    path = []
    if goal in parent or goal == start:
        node = goal
        while node != start:
            path.append(node)
            node = parent[node]
        path.append(start)
        path.reverse()
    
    return path

# 使用示例
grid = np.zeros((5, 5), dtype=int)
grid[1, 1:4] = 1
start = (0, 0)
goal = (4, 4)
path = a_star(grid, start, goal)
print("A*找到的路径:", path)

你看,A*和Dijkstra的代码结构几乎一样。唯一的区别就是:Dijkstra用dist(即g(n))作为优先级,而A*用f(n) = g(n) + h(n)作为优先级。

4.4 Dijkstra vs A*:到底选哪个?

很多新手会问:既然A*更快,为什么还要学Dijkstra?

我的回答是:没有银弹。两种算法各有适用场景。

对比维度 Dijkstra A*
最优性 保证全局最优 保证最优(如果h(n)是可采纳的)
搜索效率 低,搜索范围大 高,有方向性
适用场景 地图小、需要绝对最优路径 地图大、实时性要求高
实现复杂度 简单 稍复杂(需要设计h(n))

我个人的经验是:如果地图小于100x100,用Dijkstra也无妨。如果地图更大,或者需要实时规划,果断上A*。

4.5 知识体系总览

为了让你更直观地理解这一章的知识结构,我画了一张流程图。它展示了从栅格地图到路径规划的核心逻辑。

全局路径规划知识体系 栅格地图构建 Dijkstra算法 A*算法 全局最优 · 搜索范围大 有方向性 · 效率高 输出:最优路径

这张图把本章的核心逻辑串起来了。从栅格地图出发,根据实际需求选择Dijkstra或A*,最终输出一条最优路径。

4.6 一些掏心窝的话

最后,我想说几句实在的。

算法是死的,场景是活的。我在项目中见过太多人死磕A*的启发式函数,结果忽略了地图本身的问题。比如,栅格地图的障碍物膨胀没做好,导致路径绕远路。这时候你优化算法代码,不如回头检查一下地图。

另外,不要迷信“最优路径”。有时候,次优路径但更平滑、更安全,反而更好。我习惯在A*找到路径后,加一个路径平滑的后处理步骤。这个我们后面会讲到。

好了,这一章就到这里。代码都给你了,建议你亲手跑一跑,改一改参数,看看效果。纸上得来终觉浅,绝知此事要躬行。

可可精品资料库