最短路问题——Dijkstra 算法

什么是最短路问题

给你一张 n 个点、m 条边的有向图,每条边带一个非负权值,再给一个起点 s,问你从 s 出发到其他所有点的最短距离分别是多少。

比如样例:

复制代码
4 6 1
1 2 2
2 3 2
2 4 1
1 3 5
3 4 3
1 4 4

从 1 出发,到 1 距离是 0,到 2 是 2,到 3 是 4(1→2→3),到 4 是 3(1→2→4),答案就是 0 2 4 3。

如果暴力搜所有路径,复杂度爆炸,必须用更聪明的方法。Dijkstra 就是解决非负权图单源最短路的标准算法。


Dijkstra 算法核心思想

Dijkstra 是一种贪心算法,整体思路非常直白:

每次从"还没确定最短距离"的点里,挑一个当前距离起点最近的点,把它标记为"已确定",然后用这个点去更新它所有邻居的距离。重复这个过程,直到所有点的距离都被确定。

这能成立的前提是所有边权非负,这样先被确定的点距离一定是最短的,后面不可能再被更新得更小。


一步步手动模拟

用上面的样例图手动跑一遍,理解得更透。

初始化:d[1]=0,其他点 d[i]=∞,起点 s=1。

第一轮

未确定点中距离最小的是 1(距离 0)。

确定 1,用它的出边更新邻居:

  • 1→2 权 2:d[2]=min(∞,0+2)=2
  • 1→3 权 5:d[3]=5
  • 1→4 权 4:d[4]=4

现在 d = [0,2,5,4],确定集合 {1}。

第二轮

未确定点中距离最小的是 2(距离 2)。

确定 2,更新邻居:

  • 2→3 权 2:d[3]=min(5,2+2)=4(变短了!)
  • 2→4 权 1:d[4]=min(4,2+1)=3

现在 d = [0,2,4,3],确定集合 {1,2}。

第三轮

未确定点中距离最小的是 4(距离 3)。

确定 4,4 没有出边,不更新。

第四轮

最后确定 3(距离 4),结束。

最终得到 d = [0,2,4,3],和答案一致。整个过程就是不断"找最小 → 确定 → 松弛"的循环。


为什么需要堆优化

上面每轮找最小距离点需要扫描所有未确定点,一次 O(n),n 轮就是 O(n²)。当 n 到 10⁵ 时,必然超时。

优化思路很自然:用**小根堆(优先队列)**来维护当前距离最小的点,每次取堆顶就是最近点,复杂度直接降到 O(log n),整体变成 O((n+m) log n),足以应对题目数据。


代码模板

下面给出模板:

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
#define rep(i,a,n) for(ll i=a;i<=n;++i)
typedef long long ll;
const int maxn=1e5+10;
const ll oo=1e18;               // 无穷大,开到足够大

struct node{
    ll x;                       // 点编号
    ll d;                       // 当前最短距离
    bool operator < (const node &a) const{
        return d > a.d;         // 小根堆:距离小的排在堆顶
    }
};

ll n,m,s,d[maxn];
vector<node> G[maxn];    // 邻接表,pair<邻居, 边权>

void Dij(){
    rep(i,1,n) d[i]=oo;
    priority_queue<node> q;
    q.push({s,0});
    d[s]=0;
    while(!q.empty()){
        ll x=q.top().x, dis=q.top().d;
        q.pop();
        if(dis!=d[x]) continue;          // 懒惰删除:这个记录已过时
        for(auto [y,w]:G[x]){            // C++17 结构化绑定
            if(d[y] > d[x]+w){
                d[y]=d[x]+w;
                q.push({y, d[y]});
            }
        }
    }
}

void sol(){
    cin>>n>>m>>s;
    rep(i,1,m){
        ll u,v,w;
        cin>>u>>v>>w;
        G[u].push_back({v,w});
    }
    Dij();
    rep(i,1,n) cout<<d[i]<<' ';
}

int main(){
    sol();
    return 0;
}

代码细节逐行拆解

很多人背了板子却说不清每行在干什么,这里一次讲透。

1. 结构体与优先队列

cpp 复制代码
struct node{
    ll x,d;
    bool operator < (const node &a) const{
        return d > a.d;
    }
};

x 是点编号,d 是距离。重载 < 时返回 d > a.d,相当于告诉优先队列:距离大的 node 反而"小",这样距离最小的会排到堆顶,实现小根堆。

2. 无穷大初始值

cpp 复制代码
rep(i,1,n) d[i]=oo;

把所有点的初始距离设为无穷大,代表"暂时不可达"。oo 要设得比最大可能路径和更大,1e18 足够安全。

3. 懒惰删除

cpp 复制代码
if(dis!=d[x]) continue;

同一个点可能因为多次松弛而被反复 push 进堆,堆里会残留很多旧的距离记录。当取出的 dis 和当前 d[x] 不一致时,直接扔掉这个过时记录。这是堆优化正确性的关键。

4. 松弛操作

cpp 复制代码
if(d[y] > d[x]+w){
    d[y]=d[x]+w;
    q.push({y, d[y]});
}

"从 x 绕路到 y 会不会更短?"如果更短就更新 d[y] 并推入新记录。这里的 w 是从 x 到 y 的边权。

5. 存图方式

cpp 复制代码
vector<node> G[maxn];
// 加边
G[u].push_back({v,w});

邻接表,每个点存一个 vector,里面放 (邻居, 权值)。无向图记得正反存两次。


时间复杂度分析

  • 每个点最多被加入堆多次,但每条边最多引起一次松弛,总入堆次数 O(m)。
  • 每次堆操作 O(log n),因此总复杂度 O((n+m) log n)。
  • 对于正常的数据,这个复杂度完全没问题。

常见错误与注意事项

  1. 忘记懒惰删除

    不写 if(dis!=d[x]) continue; 会让程序重复处理过时节点,答案可能没错但会慢很多,甚至超时。

  2. 无穷大设太小

    边权最大 10⁹,路径可能累加很多条,必须用 long long,无穷大至少设 1e15 以上。

  3. 堆写成大根堆

    如果没有重载 < 且没用 greater,默认是大根堆,取出的会是距离最大的点,直接全错。

  4. 只用于非负权图

    存在负权边时 Dijkstra 失效,必须用 Bellman-Ford 或 SPFA。

  5. 忘记把 ds 设成 0

    这是起点到自己距离,漏掉的话所有点的距离都算不出来。

  6. 有向图 vs 无向图

    本题是有向图,只需要存一条边。无向图要正反都存。


总结

  • Dijkstra 是解决非负权图单源最短路的标配算法。
  • 核心是"贪心找最近点 + 松弛",用堆优化可以做到 O((n+m) log n)。
  • 模板里的 if(dis!=d[x]) continue; 是堆优化的精髓,一定不能省。
  • 手动模拟,理解每一行代码的意思比只背模板有用多了,否则一变化就不会了
相关推荐
微尘hjx几秒前
【Deep-OC-SORT 02】多轨迹算法Deep-OC-SORT-相对 OC-SORT 的改进点
人工智能·算法·yolo·目标跟踪·视觉检测·ultralytics·deep-oc-sort
梦帮科技2 分钟前
意识启发式量子隧穿退火算法用于全局优化 (CITA) 完整技术指南
网络·人工智能·算法·量子计算·模拟退火算法
一去不复返的通信er1 小时前
排序 + 双指针:力扣15三数之和
算法·leetcode
落樱弥城1 小时前
Perf 逐步探查性能缺口并优化
c++·性能优化
Pniubi1 小时前
力扣3无重复字符的最长字串(滑动窗口)
算法·leetcode·职场和发展
坤坤子吖2 小时前
Linux网络编程:UDP进阶,从字典服务器到简单聊天室
linux·服务器·网络·c++·笔记·学习
On The Road :2 小时前
深入理解 JVM 垃圾回收:GC 是什么、如何判定垃圾、四大回收算法全解析
jvm·算法
Rabitebla2 小时前
【Linux学习笔记】进程篇(九):进程的创建与退出——fork、写时拷贝与退出码
linux·c++·笔记·学习
时间的拾荒人2 小时前
CMake 实战指南:从基础语法到静态库/动态库制作与链接
开发语言·c++·面试
道尔柯南2 小时前
C++ STL 入门详解:从认识标准模板库到掌握学习方法
java·c++·学习方法