news 2026/4/23 23:07:25

C++ 版Dijkstra 算法详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 过程示意(无图形)

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 代码(注释充分)

#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 代码示例(堆优化)

#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 辅助仅作直观演示,真的代码可用任何可视化工具生成。

顶点: 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您已确定:00+2<∞→2(1) ,0+5<∞→5(2)[0,2,5,∞,∞]
2取最小非访问:112+1<5→3(2) ,2+2<∞→4(3)[0,2,3,4,∞]
3取最小非访问:223+3<4?3+3=6>4(不更新)同上
4取最小非访问:334+1<∞→5(4)[0,2,3,4,5]
5取最小非访问:44完成

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

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

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 输入格式(示例)

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 运行结果

0 2 3 4 5

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

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

// ① 基本实现(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++ 环境里实现从 “源点到其余点的最短路径” 的任务,且知道何时选择哪种实现方式。祝编码愉快 🚀

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/23 23:03:16

Java学习13

上午&#xff08;2.5h&#xff09;多态核心 向上 / 向下转型1. 多态的 3 个前提条件&#xff08;0.5h&#xff09;必须同时满足&#xff0c;缺一不可必须有继承关系&#xff08;extends&#xff09;子类必须重写父类方法&#xff08;override&#xff09;父类引用指向子类对象…

作者头像 李华
网站建设 2026/4/23 23:01:03

保姆级教程:在Ubuntu 20.04 ROS Noetic下,用奥比中光Astra Pro完成相机标定(附常见报错解决)

奥比中光Astra Pro相机标定全流程实战&#xff1a;从环境搭建到避坑指南 在机器人视觉和三维感知领域&#xff0c;相机标定是确保测量精度的基础环节。作为一款性价比较高的深度相机&#xff0c;奥比中光Astra Pro在SLAM、手势识别等场景中应用广泛。但许多开发者在ROS环境下进…

作者头像 李华
网站建设 2026/4/23 23:01:02

保姆级教程:用OpenCV和PCL库给激光雷达点云上色(附完整C++代码)

从零实现激光雷达点云着色&#xff1a;OpenCVPCL实战指南 当激光雷达扫描的稀疏点云被赋予相机捕捉的真实色彩&#xff0c;三维世界瞬间变得鲜活起来。这种融合不仅提升了数据的直观性&#xff0c;更为自动驾驶、机器人导航和三维重建等应用提供了更丰富的信息维度。本文将手把…

作者头像 李华
网站建设 2026/4/23 23:00:18

从零开发一个Illustrator条码插件:手把手教你用ExtendScript写UI和算法

从零开发Illustrator条码插件&#xff1a;ExtendScript实战指南 在平面设计和印刷领域&#xff0c;条码生成是常见的需求。虽然市面上有现成的条码生成工具&#xff0c;但作为设计师或开发者&#xff0c;掌握如何为Illustrator开发自定义插件不仅能提升工作效率&#xff0c;还能…

作者头像 李华