最短路问题——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),足以应对题目数据。


代码模板

下面给出模板:

#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. 结构体与优先队列

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

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

2. 无穷大初始值

rep(i,1,n) d[i]=oo;

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

3. 懒惰删除

if(dis!=d[x]) continue;

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

4. 松弛操作

if(d[y] > d[x]+w){
    d[y]=d[x]+w;
    q.push({y, d[y]});
}

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

5. 存图方式

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. 忘记把 d[s] 设成 0
    这是起点到自己距离,漏掉的话所有点的距离都算不出来。

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


总结

  • Dijkstra 是解决非负权图单源最短路的标配算法。
  • 核心是“贪心找最近点 + 松弛”,用堆优化可以做到 O((n+m) log n)。
  • 模板里的 if(dis!=d[x]) continue; 是堆优化的精髓,一定不能省。
  • 手动模拟,理解每一行代码的意思比只背模板有用多了,否则一变化就不会了
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值