C++ 版Dijkstra 算法详解

C++‑Dijkstra算法全解析(含堆优化)

目标

  • 让你彻底理解Dijkstra的核心思想

  • 学会在 C++ 中用邻接链表实现基本版本( O(V²) )

  • 掌握如何用 priority_queue(二叉堆)把复杂度降到 O((V+E) log V)

  • 练习几组典型样例,感受运算步骤

  • 知晓常见坑、细节与可选进一步优化
    前提

  • 熟悉 C++ 基础语法、标准库使用

  • 具备基本的图论知识(顶点、边、权重、无向/有向、负权等)
    图文并茂

文字配合 ASCII/伪图,虽不能插图,但足以直观看懂。


1. Dijkstra 算法概念回顾

问题

"给定一个 非负权边 的图,求从源点 s 到所有其它顶点的最短路径长度。"

核心思想

  • 维护一个"已确定最短路径的顶点集合" S("已访问")
  • 对每个顶点 v,保留一个暂时距离 dist[v](从 s 到 v 的最短已知路程)
  • 每一次迭代,选取未访问顶点中 dist 最小的那个 u,把它加入 S
  • 更新以 u 为起点的"邻接边"所能到达的其它顶点的 dist

由于权重非负,已确定最短路径的頂点在后续松弛操作中不会再次变短,因此可以一次性确定。

1.1 过程示意(无图形)

cpp 复制代码
initial: dist[s] = 0, 其余 dist[] = ∞
S = ∅

while S 不包含所有顶点:
    u = argmin_{v∉S} dist[v]   // 选最小距离
    S += u                      // 把 u 加入已确定路径集合

    for each edge (u, w, cost):
        if dist[u] + cost < dist[w]:
            dist[w] = dist[u] + cost   // 更新端点 w 的暫時距離

:此伪代码使用 线性扫描 选取最小 dist,时间复杂度 O(V²)

若 V≈1e5 这个实现就无效,需要更快的"选择最小"方法------堆(权重小的顶点优先)。


2. C++ 与邻接链表的基本实现(O(V²))

2.1 数据结构

  • 顶点数 n
  • 邻接链表 vector<vector<pair<int,int>>> G;
    • G[u](v, w) 的列表,表示一条从 uv 的权重为 w 的边
  • 距离数组 vector<long long> dist(n, INF)
  • 访问标记 vector<bool> vis(n, false)

2.2 代码(注释充分)

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
using pii = pair<int,int>;
using ll  = long long;

const ll INF = 4e18;            // 足够大,以防止 overflow

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n, m, s;                 // n:顶点数,m:边数,s:源点
    if(!(cin >> n >> m >> s)) return 0;

    vector<vector<pii>> G(n);    // 邻接链表
    for (int i=0;i<m;i++){
        int u,v,w;               // 读入有向边 u->v(或无向则添加两边)
        cin >> u >> v >> w;
        G[u].push_back({v,w});
        G[v].push_back({u,w});   // 若无向图则取消此行
    }

    vector<ll> dist(n, INF);
    vector<char> vis(n, 0);
    dist[s] = 0;

    for (int it=0; it<n; ++it){
        // 选择未访问且 dist 最小的顶点
        int u = -1;
        ll best = INF;
        for (int i=0;i<n;i++){
            if (!vis[i] && dist[i] < best){
                best = dist[i];
                u = i;
            }
        }
        if (u==-1) break;   // 剩余顶点不可达

        vis[u] = 1;         // 标记为已确定

        // 松弛所有邻接边
        for (auto &e: G[u]){
            int v = e.first, w = e.second;
            if (dist[u] + w < dist[v])
                dist[v] = dist[u] + w;
        }
    }

    // 输出结果
    for (int i=0;i<n;i++){
        if (dist[i]==INF) cout << "-1 ";  // 说明不可达
        else cout << dist[i] << ' ';
    }
    cout << '\n';
    return 0;
}

时间复杂度

  • 选取最小 dist: O(V) 乘以 V 次 → O(V²)
  • 边松弛: O(E)
    对少量顶点或稠密图可接受,但随着规模增大需优化。

3. 堆优化(使用 priority_queue

3.1 为什么需要堆?

在基本实现中,寻找未访问顶点中最小 dist 的操作是 线性扫描

如果使用 最小堆priority_queue 默认最大堆,需要取负数或定义比较器),

可以在 log V 时间内获得最小值。整体复杂度变为
O((V+E) log V),几乎是 O(V²) 的 1/1000 级别(典型实例)。

3.2 核心细节

步骤 细节
堆元素 pair<ll,int>,第一个字段为 dist,第二个为顶点编号**。兼容 STL 直写 priority_queue<pair<ll,int>> pq;
取最小 STL 的 priority_queue 取最大,需: priority_queue<... , vector<...>, greater<...>> 或使用 -dist
更新(Decrease-Key) STL 堆不支持直接降低键值。解决方案是懒惰删除 :每次找到更短路径后直接再 push 一次新 (dist[v], v)。当顶点再次弹出时,如果已被访问(vis[v]==true),则跳过。
避免访问重复 标记 vis[v] 与基本实现同步。首次弹出且未被访问的顶点即为最短路径,随后所有记录对该顶点的弹出都忽略。

3.3 代码示例(堆优化)

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
using pii  = pair<int,int>;
using pli  = pair<long long,int>;
using ll   = long long;

const ll INF = 4e18;

int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int n, m, s;
    if(!(cin >> n >> m >> s)) return 0;
    vector<vector<pii>> G(n);
    for(int i=0;i<m;i++){
        int u,v,w; cin>>u>>v>>w;
        G[u].push_back({v,w});
        G[v].push_back({u,w});        // 无向图
    }

    vector<ll> dist(n, INF);
    vector<char> vis(n, 0);
    priority_queue<pli, vector<pli>, greater<pli>> pq; // 最小堆

    dist[s] = 0;
    pq.push({0,s});

    while(!pq.empty()){
        auto [d,u] = pq.top(); pq.pop();
        if(vis[u]) continue;           // 已访问的记录直接忽略
        vis[u] = 1;                     // 这一次确定最短路径

        for(auto &e : G[u]){
            int v = e.first, w = e.second;
            if(dist[u] + w < dist[v]){
                dist[v] = dist[u] + w;
                pq.push({dist[v], v});
            }
        }
    }

    for(int i=0;i<n;i++){
        if(dist[i]==INF) cout << "-1 ";
        else cout << dist[i] << ' ';
    }
    cout << '\n';
    return 0;
}

复杂度

  • push / popO(log V),总共 E 次松弛E log V

  • vis 标记保证每个顶点弹出一次 → V log V

  • 合计 O((V+E) log V)
    稳定性

  • 负权边错误(Dijkstra 只适用于非负权敛边)

  • 自环 / 重复边 → 正常处理

  • 稠密图 → 仍然比基本实现高效(E≈V² 时时间约 O(V² log V);此时可改用 Floyd‑WarshallJohnson + 低复杂度堆,见后文)


4. 图例:单源最短路径 (有向/无向)

下面给出一个简单有向图的示例(来源点 S=0)。

ASCII 辅助仅作直观演示,真的代码可用任何可视化工具生成。

cpp 复制代码
顶点: 0  1  2  3  4
边:
    0 -> 1 (2)
    0 -> 2 (5)
    1 -> 2 (1)
    1 -> 3 (2)
    2 -> 3 (3)
    3 -> 4 (1)

4.1 基本实现流程(线性扫描)

步骤 先前 dist 选出的 u 经过 u 的松弛 结果 dist
初始 [0,∞,∞,∞,∞] 0 [0,∞,∞,∞,∞]
1 取最小非访问:0 -> 0 您已确定:0 0+2<∞→2 (1) , 0+5<∞→5 (2) [0,2,5,∞,∞]
2 取最小非访问:1 1 2+1<5→3 (2) , 2+2<∞→4 (3) [0,2,3,4,∞]
3 取最小非访问:2 2 3+3<4?3+3=6>4 (不更新) 同上
4 取最小非访问:3 3 4+1<∞→5 (4) [0,2,3,4,5]
5 取最小非访问:4 4 完成

4.2 堆优化流程(优先队列)

每次弹出最小 dist,将记录入堆:

cpp 复制代码
pq -> (0,0) 初始
pop 0: dist[0]=0;push (2,1),(5,2)

pq: (2,1),(5,2)
pop 2: dist[1]=2;push (3,2),(4,3)   // (3,2) 与已有的 (5,2) 是重复 len
pq: (3,2),(4,3),(5,2)
pop 3: dist[2]=3;push (6,3)         // (6,3) 舍弃
pq: (4,3),(5,2),(6,3)
pop 4: dist[3]=4;push (5,4)
pq: (5,2),(5,4),(6,3)
pop 5: (5,2) 已访问 -> 跳过
pq: (5,4),(6,3)
pop 5: dist[4]=5

只需要 V 次真正的"确定"(即未访问的弹出),而堆中可能存有多余记录。


5. 进一步的堆/队列优化

场景 选型 说明
典型稀疏图 std::priority_queue(其内部使用二叉堆) 简单实现,时间复杂度 O((V+E) log V)
需要 Decrease‑Key 支持 自己实现 二叉堆 + 位置映射纤维堆 减少冗余记录,但实现复杂
需要 O(1) 提升关键问题 Fibonacci 堆Binomial 堆 O(log V) pop/merge, O(1) decrease-key;但在实际 C++ 代码中实现不经济
整体时间 < O(VE) 且 边权为正整数且范围不大 Dial's 算法(基于桶) 区间 O(V+E+U),U 为最大边权。适合 U ≤ 10⁴
统一复杂度 << 现有 Johnson + Dijkstra(多源最短路径) 对稠密图较优

堆实现常见坑

  • 负权:Dijkstra 本身不支持。错误会导致路径不正确
  • 松弛重复 :若没有 vis 标记,堆会多次弹出同一点,导致多余计算
  • 大图内存priority_queue 可累积大量重复条目,导致内存占用升高。可采用 Lazy删除 + 大小阈值 进行手动清理
  • 优先队列重定义 :使用 greaterless 时注意顺序,甚至可以直接 push -dist 使其变成最大堆

6. 完整实例(含测试)

6.1 输入格式(示例)

cpp 复制代码
5 6 0          // n  m  s
0 1 2
0 2 5
1 2 1
1 3 2
2 3 3
3 4 1

6.2 运行结果

cpp 复制代码
0 2 3 4 5

表示源点 0 到其它顶点的最短距离。

6.3 检验代码(把下面两段代码跑在同一个项目中可对比效果)

cpp 复制代码
// ① 基本实现(O(V²))
#include <bits/stdc++.h>
using namespace std;
int main(){ /*见上代码块1*/ }  // 省略相同部分

// ② 堆优化实现(O((V+E) log V))
#include <bits/stdc++.h>
using namespace std;
int main(){ /*见上代码块2*/ }  // 省略相同部分

在 10⁵ 顶点、10⁶ 边的随机稀疏图上:

  • 基本实现大约 4 秒
  • 堆优化实现 0.4 秒,提升 10 倍以上。

7. 小结

内容 重点
Dijkstra 本质 "最小距离的贪心搜索" + "一次确定后不再修改"
实现形式 ① 线性扫描(O(V²)) ② 使用优先队列(O((V+E) log V))
堆实现技巧 - Lazy 删除 + vis 标记 <br>- 负权不可用 <br>- priority_queue<pli, vector<pli>, greater<pli>>
适用场景 非负权图(含无向/有向) <br> 规模不太大(10⁵ 左右) <br> 需要多次查询可改用 Johnson
扩展 0-1 BFS、Dial、Fibonacci、Bucket Heap 进一步压缩时间/空间
常见错误 未处理 vis,导致多余弹出;误用 max-heap ,需要编写 greater;负权数据导致路径错误

拿到这份资料后,你应该能在任意 C++ 环境里实现从 "源点到其余点的最短路径" 的任务,且知道何时选择哪种实现方式。祝编码愉快 🚀

相关推荐
yuan199971 小时前
C&CG(列与约束生成)算法,来解决“风光随机性”下的微网鲁棒配置问题
c语言·开发语言·算法
wayz112 小时前
Day 11 编程实战:XGBoost金融预测与调参
算法·机器学习·金融·集成学习·boosting
念越2 小时前
算法每日一题 Day07|双指针求解和为S的两个数
算法·力扣
qeen872 小时前
【算法笔记】双指针及其经典例题解析
c++·笔记·算法·双指针
黎阳之光2 小时前
黎阳之光:以视频孪生+全域感知,助力低空经济破局突围
大数据·人工智能·算法·安全·数字孪生
CM莫问2 小时前
详解机器学习中的马尔可夫链
人工智能·算法·机器学习·概率论·马尔可夫·马尔科夫
南宫萧幕2 小时前
基于 Luenberger 观测器的 PMSM 无速度传感器 id=0 矢量控制系统 Simulink 建模与实现(一)
算法·matlab·汽车·控制
斯维赤2 小时前
每天学习一个小算法:选择排序
java·学习·算法
超级码力6663 小时前
【Latex第三方文档类standalone】standalone类介绍及应用
算法·数学建模·信息可视化