4. 环境建模与地图表示

说到全局路径规划,Dijkstra算法是个绕不开的基础。我刚开始做机器人导航那会儿,第一个接触的规划算法就是它。说实话,这算法看着简单,但真正用好它,还是有不少门道的。

4.1 算法核心思想

Dijkstra算法要解决什么问题?说白了就是:在带权图中,找到从起点到所有其他节点的最短路径。它是个贪心算法,每次选当前代价最小的节点去扩展。

我记得第一次在项目里用它,是在一个仓储机器人上。地图有几十个节点,机器人要从A点去B点取货。当时我直接用Dijkstra跑了一遍,路径是出来了,但总觉得哪里不对劲。后来才发现——我忘了考虑路径的平滑性。嗯,这是后话了。

算法的核心逻辑其实就三步:

  1. 初始化:起点代价为0,其他节点代价为无穷大
  2. 选择:从未处理的节点中,选代价最小的那个
  3. 松弛:更新该节点所有邻居的代价

重复2和3,直到所有节点都被处理完。

关键点:Dijkstra要求图中所有边的权值必须为非负数。如果有负权边,算法会失效。我曾经踩过这个坑——地图里有个"下坡路段"设成了负权值,结果路径全乱了。

4.2 算法流程详解

我们用一个具体例子来走一遍流程。假设有这样一个图:

Dijkstra算法示例图 A B C D E 4 2 5 1 3 2 起点A,目标E。边上的数字是代价(距离/时间)。 Dijkstra会先找到A→C(代价2),然后从C继续扩展...

我们一步步来:

步骤 当前节点 已处理集合 距离记录
0 A {A} A:0, B:∞, C:∞, D:∞, E:∞
1 A→C {A, C} A:0, B:4, C:2, D:∞, E:∞
2 C→B {A, C, B} A:0, B:3, C:2, D:3, E:∞
3 B→D {A, C, B, D} A:0, B:3, C:2, D:3, E:5
4 D→E {A, C, B, D, E} A:0, B:3, C:2, D:3, E:5

你看,最终从A到E的最短路径是A→C→D→E,总代价5。而不是A→B→D→E(代价7)。

4.3 代码实现

我习惯用Python来实现Dijkstra。为什么?因为项目里做原型验证快,而且代码可读性好。下面是我在项目中常用的实现:

import heapq

def dijkstra(graph, start):
    """
    graph: 邻接表表示的图,格式 {node: [(neighbor, cost), ...]}
    start: 起点
    返回: (dist, prev) 距离字典和前驱节点字典
    """
    # 初始化
    dist = {node: float('inf') for node in graph}
    dist[start] = 0
    prev = {node: None for node in graph}
    
    # 优先队列,存 (距离, 节点)
    pq = [(0, start)]
    visited = set()
    
    while pq:
        current_dist, current = heapq.heappop(pq)
        
        # 如果当前节点已处理,跳过
        if current in visited:
            continue
        
        visited.add(current)
        
        # 遍历邻居
        for neighbor, cost in graph[current]:
            if neighbor in visited:
                continue
            
            new_dist = current_dist + cost
            
            # 松弛操作
            if new_dist < dist[neighbor]:
                dist[neighbor] = new_dist
                prev[neighbor] = current
                heapq.heappush(pq, (new_dist, neighbor))
    
    return dist, prev

def reconstruct_path(prev, start, goal):
    """重建从start到goal的路径"""
    path = []
    current = goal
    while current is not None:
        path.append(current)
        current = prev[current]
    path.reverse()
    return path if path[0] == start else []

# 使用示例
graph = {
    'A': [('B', 4), ('C', 2)],
    'B': [('A', 4), ('C', 3), ('D', 5)],
    'C': [('A', 2), ('B', 3), ('D', 1)],
    'D': [('B', 5), ('C', 1), ('E', 2)],
    'E': [('D', 2)]
}

dist, prev = dijkstra(graph, 'A')
path = reconstruct_path(prev, 'A', 'E')
print(f"最短路径: {' → '.join(path)}")  # A → C → D → E
print(f"总代价: {dist['E']}")  # 5

我的经验:用heapq实现优先队列是关键。如果自己写排序,时间复杂度会从O((V+E)logV)退化到O(V²)。在节点数超过1000的地图上,差距非常明显。

4.4 算法复杂度分析

你可能会问:Dijkstra到底快不快?这得看实现方式。

实现方式 时间复杂度 适用场景
朴素数组 O(V²) 稠密图(边多)
二叉堆(heapq) O((V+E)logV) 稀疏图(边少),最常用
斐波那契堆 O(VlogV+E) 理论最优,但实现复杂

我在实际项目中,90%的情况都用二叉堆实现。为什么?因为机器人地图通常是稀疏图——每个节点只连接周围几个邻居,边数远小于V²。二叉堆已经够用了。

4.5 避坑指南

我曾经踩过的坑:

  • 负权边:有一次地图里有个"捷径"设成了负值,Dijkstra直接死循环了。记住,有负权边要用Bellman-Ford。
  • 大图内存爆炸:10000个节点的全连接图,邻接矩阵要存1亿个元素。我后来改用邻接表,内存从800MB降到了20MB。
  • 路径不唯一:当多条路径代价相同时,Dijkstra只返回其中一条。如果你需要所有最短路径,得改算法。

4.6 在机器人规划中的实际应用

Dijkstra在机器人领域最常见的用法是:在拓扑地图上做全局路径规划。比如:

  • 仓储机器人:货架间的路径规划,节点是货架位置,边是通道距离
  • 扫地机器人:房间到房间的导航,节点是房间中心,边是门之间的距离
  • 自动驾驶:道路网络中的路径规划,节点是路口,边是路段长度

我个人习惯是:先用Dijkstra算出一条全局路径,再用局部规划器(比如DWA)做平滑和避障。这样既保证了全局最优,又兼顾了局部灵活性。

一个实用技巧:如果地图很大(比如几万个节点),可以用双向Dijkstra——从起点和终点同时搜索,相遇时停止。速度能快2-4倍。我在一个物流项目里用过,搜索时间从3秒降到了0.8秒。

4.7 小结

Dijkstra算法是全局路径规划的基石。它简单、可靠、保证最优。但你要记住它的限制:不能有负权边,在大图上要考虑性能优化。

嗯,掌握了Dijkstra,后面学A*算法就会轻松很多——A*其实就是Dijkstra加了个启发式函数。下节课我们会看到,加了"方向感"之后,搜索效率能提升多少。