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) 的列表,表示一条从 u 到 v 的权重为 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;
}

时间复杂度

  • 选取最小 distO(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 / pop 各 O(log V),总共 E 次松弛 → E log V
  • vis 标记保证每个顶点弹出一次 → V log V
  • 合计 O((V+E) log V)

稳定性

  • 负权边 → 错误(Dijkstra 只适用于非负权敛边)
  • 自环 / 重复边 → 正常处理
  • 稠密图 → 仍然比基本实现高效(E≈V² 时时间约 O(V² log V);此时可改用 Floyd‑Warshall 或 Johnson + 低复杂度堆,见后文)

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删除 + 大小阈值 进行手动清理
  • 优先队列重定义:使用 greater 或 less 时注意顺序,甚至可以直接 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++ 环境里实现从 “源点到其余点的最短路径” 的任务,且知道何时选择哪种实现方式。祝编码愉快 🚀

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值