4. Dijkstra算法:算法原理、优先队列优化、在栅格地图上的仿真实现

Dijkstra算法,说实话,是我个人在路径规划领域接触的第一个真正意义上的「全局搜索」算法。记得刚入行那会儿,我还在用BFS做简单寻路,结果在带权重的栅格地图上碰了一鼻子灰——明明看起来很近的路,走起来却绕了一大圈。后来导师丢给我一句话:「去把Dijkstra搞明白,你就知道什么叫最优了。」

好,咱们今天就把这个算法彻底聊透。我会从原理讲起,再聊工程上的优化,最后带你看一个栅格地图上的仿真实现。

4.1 算法核心思想:贪心 + 松弛

Dijkstra算法的本质,说白了就是一句话:从起点开始,每次选一个当前已知「距离最近」且没处理过的节点,然后尝试用它去更新邻居的距离

这个过程,专业术语叫「松弛操作」。你想想看,就像你手里有一张地图,你每到一个新路口,就看看能不能通过这个路口让去其他地方的路变得更短。能,就更新;不能,就拉倒。

核心三要素:

  • dist[] 数组:记录从起点到每个节点的当前最短距离
  • visited[] 数组:标记哪些节点已经找到了最短路径
  • 优先队列:每次快速取出「当前距离最小」的节点

嗯,这里要注意:Dijkstra要求所有边的权重必须是非负的。为什么?因为一旦有负权边,你之前认为「已经最短」的路径,可能被后面一条负权边给「反超」了。我在项目中遇到过这种坑——用Dijkstra跑一个带负权回路的图,结果程序死循环了。后来改用Bellman-Ford才解决。

4.2 算法步骤详解

咱们一步步来,我习惯用伪代码先理清逻辑:

function Dijkstra(Graph, source):
    dist[source] = 0
    for each vertex v in Graph:
        if v != source:
            dist[v] = INF
        add v to priority queue Q with priority dist[v]

    while Q is not empty:
        u = Q.extract_min()          // 取出当前距离最小的节点
        for each neighbor v of u:
            alt = dist[u] + weight(u, v)
            if alt < dist[v]:
                dist[v] = alt
                Q.decrease_key(v, alt)  // 更新优先队列中的优先级
    return dist[]

你看,逻辑其实很简洁。但工程实现上,有几个细节值得聊一聊。

4.3 优先队列优化:从 O(V²) 到 O((V+E)logV)

如果不做优化,每次找「当前距离最小节点」都要遍历整个dist数组,复杂度是O(V²)。这在节点数一多(比如栅格地图1000×1000,那就是100万个节点)时,根本跑不动。

优化的思路很直接:用最小堆(优先队列)来维护待处理节点。每次取堆顶元素,复杂度降为O(logV)。

我个人习惯用C++的std::priority_queue配合pair<int, int>来实现。但要注意一个坑:标准库的优先队列不支持decrease_key操作。怎么办?

我的做法是:直接push新的pair进去,旧的不管它。取出时判断一下,如果当前节点的dist值已经比队列里记录的大,就跳过。这叫「惰性删除」,工程上非常实用。

避坑指南:我曾经在嵌入式平台上用Dijkstra,堆的实现用了STL的priority_queue,结果发现内存占用比预期大很多。后来改用std::set模拟堆,既支持decrease_key,又节省了内存。具体场景要具体分析。

4.4 栅格地图上的仿真实现

好,理论聊完了,咱们上点干货。下面是一个在栅格地图上实现Dijkstra的Python示例。地图用二维数组表示,0是空地,1是障碍物。

import heapq
import numpy as np

def dijkstra_grid(grid, start, goal):
    rows, cols = grid.shape
    # 4方向移动:上、下、左、右
    dirs = [(-1,0), (1,0), (0,-1), (0,1)]
    
    # 初始化距离矩阵
    dist = np.full((rows, cols), np.inf)
    dist[start] = 0
    
    # 优先队列:(距离, 行, 列)
    pq = [(0, start[0], start[1])]
    # 记录路径
    parent = {}
    
    while pq:
        d, r, c = heapq.heappop(pq)
        
        # 惰性删除:如果当前距离比记录的大,跳过
        if d > dist[r, c]:
            continue
        
        # 到达终点
        if (r, c) == goal:
            break
        
        # 遍历邻居
        for dr, dc in dirs:
            nr, nc = r + dr, c + dc
            # 边界检查 & 障碍物检查
            if 0 <= nr < rows and 0 <= nc < cols and grid[nr, nc] == 0:
                new_dist = d + 1  # 每步代价为1
                if new_dist < dist[nr, nc]:
                    dist[nr, nc] = new_dist
                    parent[(nr, nc)] = (r, c)
                    heapq.heappush(pq, (new_dist, nr, nc))
    
    # 回溯路径
    path = []
    if (goal) in parent or goal == start:
        cur = goal
        while cur != start:
            path.append(cur)
            cur = parent[cur]
        path.append(start)
        path.reverse()
    
    return path, dist

注意:上面这个实现假设每步代价为1。如果你的栅格地图中不同区域移动代价不同(比如草地、沙地),只需要把new_dist = d + 1改成new_dist = d + cost[nr][nc]即可。我在做农业机器人项目时,就遇到过不同地形代价不同的问题,Dijkstra处理起来非常自然。

4.5 知识体系与核心逻辑

下面这张图,是我自己总结的Dijkstra算法知识体系。你看一眼,就能把整个章节串起来。

Dijkstra算法知识体系 算法原理 • 贪心策略:每次选最近节点 • 松弛操作:更新邻居距离 • 非负权边要求 优先队列优化 • 最小堆维护待处理节点 • 惰性删除技巧 • O((V+E)logV) 复杂度 栅格地图实现 • 二维数组表示地图 • 4/8方向邻居搜索 • 路径回溯 核心逻辑总结 初始化 → 取最小 → 松弛邻居 → 重复直到队列为空 典型应用场景 • 机器人全局路径规划 • 地图导航系统 • 网络路由协议

4.6 几个值得注意的工程细节

最后,分享几个我在实际项目中踩过的坑:

  • 地图太大怎么办? 如果栅格地图超过1000×1000,Dijkstra的搜索空间会非常大。我建议先做地图降采样,或者改用A*算法(带启发式)。
  • 路径不平滑怎么办? Dijkstra出来的路径是栅格级的,会有锯齿。我习惯在后面加一个路径平滑步骤,比如用B样条曲线拟合。
  • 动态障碍物怎么处理? Dijkstra是静态算法。如果环境变化,需要重新规划。我在项目中用过D* Lite来做动态重规划,效果不错。

一个小技巧:如果你用Python做仿真,可以用heapq库实现优先队列。它比你自己手写堆要稳定得多。我早期犯过傻,自己写了个二叉堆,结果在边界条件上出了bug,调试了一整天。

好了,Dijkstra算法就聊到这儿。它的思想其实很简单——贪心地扩展最短路径边界。但正是这个简单的思想,构成了后续A*、D*等更高级算法的基础。你把它吃透了,后面的路就好走了。


公众号:蓝海资料掘金营,微信deep3321