4、单目标路径规划基础:Dijkstra算法原理、实现与局限性

各位同学,今天我们来聊聊路径规划里最经典的一个算法——Dijkstra。说实话,我入行那会儿,第一个认真啃下来的算法就是它。那时候还在做地面机器人巡检的项目,地图不大,但要求绝对最优解。Dijkstra虽然慢,但胜在稳,结果可解释性强。直到现在,我在做多目标航迹规划时,底层依然会用它做基准校验。

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

Dijkstra算法的本质,说白了就是一句话:从起点出发,每次选一个当前距离最短的未处理节点,然后尝试用它去更新邻居的距离。这个过程叫“松弛”(Relaxation)。

你想想看,这像不像我们平时做决策?手里有一堆待办事项,先挑最紧急的那个处理掉,处理完再看看有没有因为这件事而变得更容易解决的其他事。嗯,就是这个道理。

核心前提:Dijkstra要求图中所有边的权重必须为非负数。为什么?因为一旦有负权边,当前选出的“最短距离”节点可能并不是真正的全局最短,后面再松弛时可能会发现更短的路径。这个坑,我踩过。

4.2 算法步骤详解

我个人习惯把Dijkstra拆成三个关键步骤来记:

  1. 初始化:起点距离设为0,其他节点距离设为无穷大。所有节点标记为“未访问”。
  2. 选择:从未访问节点中,选出距离最小的那个节点u。
  3. 松弛:遍历u的所有邻居v,如果 dist[u] + w(u,v) < dist[v],就更新dist[v],并记录前驱节点。

重复步骤2和3,直到所有节点都被访问过,或者目标节点被访问到(可以提前终止)。

我的小技巧:在实际工程中,我通常用优先队列(最小堆)来维护“未访问节点集合”,这样每次选最小距离节点的时间复杂度能从O(n)降到O(log n)。

4.3 代码实现:C++ 版本

下面是我在项目中常用的一个实现模板。注意,这里我用的是邻接表 + 优先队列,适合稀疏图。

#include <vector>
#include <queue>
#include <limits>

using namespace std;

const int INF = numeric_limits<int>::max();

// 图结构:邻接表,每个元素为 (邻居节点, 边权)
typedef pair<int, int> Edge;
vector<vector<Edge>> graph;

vector<int> dijkstra(int start, int n) {
    vector<int> dist(n, INF);
    dist[start] = 0;
    
    // 优先队列:存储 (距离, 节点编号),按距离升序
    priority_queue<Edge, vector<Edge>, greater<Edge>> pq;
    pq.push({0, start});
    
    while (!pq.empty()) {
        int d = pq.top().first;
        int u = pq.top().second;
        pq.pop();
        
        // 如果当前取出的距离比记录的大,说明是旧数据,跳过
        if (d > dist[u]) continue;
        
        for (auto &edge : graph[u]) {
            int v = edge.first;
            int w = edge.second;
            if (dist[u] + w < dist[v]) {
                dist[v] = dist[u] + w;
                pq.push({dist[v], v});
            }
        }
    }
    return dist;
}

注意:代码中 if (d > dist[u]) continue; 这一行非常关键。我曾经在早期版本中漏掉它,结果优先队列里堆积了大量过期数据,导致内存暴涨。这是Dijkstra实现中最常见的bug之一。

4.4 算法复杂度分析

实现方式 时间复杂度 空间复杂度 适用场景
朴素数组实现 O(V²) O(V) 稠密图(边数接近V²)
二叉堆 + 邻接表 O((V+E)logV) O(V+E) 稀疏图(大多数实际场景)
斐波那契堆 + 邻接表 O(VlogV + E) O(V+E) 理论最优,工程实现复杂

在实际的无人机航迹规划中,我们的图通常是稀疏的——每个节点只连接附近几个邻居。所以二叉堆实现是首选。我记得有一次做城市低空航路规划,节点数上万,用二叉堆Dijkstra跑一次也就几十毫秒,完全够用。

4.5 局限性:为什么不能直接用于多目标规划?

Dijkstra虽然经典,但用在多目标航迹规划上,有几个硬伤:

  • 单源单目标效率低:如果只求一个目标点,Dijkstra仍然会探索大量无关区域。A*算法通过启发式函数解决了这个问题。
  • 无法处理负权边:这在无人机场景中不常见,但如果你用“时间窗”或“惩罚项”建模,可能会出现负权。
  • 多目标时重复计算:如果有N个目标点,你需要跑N次Dijkstra。这在实时规划中是不可接受的。
  • 不直接支持约束:比如无人机最大转弯角、禁飞区等,Dijkstra本身不处理这些,需要额外改造。

我的经验:在《多目标航迹规划与冲突解决方案》课程中,Dijkstra更多是作为“基准算法”存在。我们用它来验证更高级算法的结果是否正确。比如,我会先跑一次Dijkstra得到理论最优解,然后对比启发式算法(如A*、RRT*)的误差。

4.6 知识体系流程图

下面我用一张SVG图来总结本章的核心逻辑,方便你建立整体认知:

Dijkstra算法知识体系 核心原理 贪心选择 + 松弛操作 ① 初始化 ② 选择最小距离节点 ③ 松弛邻居节点 循环直到所有节点访问 实现方式 局限性 应用场景 • 朴素数组:O(V²) • 二叉堆:O((V+E)logV) • 斐波那契堆:O(VlogV+E) • 不能处理负权边 • 多目标重复计算 • 不支持复杂约束 • 基准算法验证 • 静态路网规划 • 通信网络路由

4.7 避坑指南与工程建议

最后,分享几个我在实际项目中积累的经验:

  • 关于无穷大的选择:不要用 INT_MAX,因为做加法时会溢出。我习惯用 1e90x3f3f3f3f,后者在C++里加两倍也不会溢出。
  • 关于提前终止:如果只求到某个目标点的最短路径,当目标节点被弹出优先队列时就可以停了。这能省下不少时间。
  • 关于路径重建:别忘了维护前驱数组。我见过有人只算距离不存路径,结果跑完发现不知道路怎么走——这在实际中毫无意义。
  • 关于动态变化:如果地图是动态的(比如临时禁飞区),Dijkstra需要重新跑。这时候可以考虑LPA*或D* Lite等增量算法。

一句话总结:Dijkstra是路径规划的“Hello World”。它简单、正确、可解释,但效率有限。掌握它是理解更高级算法(A*、D*、RRT等)的基石。下一章我们会看到,如何通过加入启发式信息,把Dijkstra升级成A*算法。


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