什么是最短路问题
给你一张 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)。
- 对于正常的数据,这个复杂度完全没问题。
常见错误与注意事项
-
忘记懒惰删除
不写if(dis!=d[x]) continue;会让程序重复处理过时节点,答案可能没错但会慢很多,甚至超时。 -
无穷大设太小
边权最大 10⁹,路径可能累加很多条,必须用long long,无穷大至少设1e15以上。 -
堆写成大根堆
如果没有重载<且没用greater,默认是大根堆,取出的会是距离最大的点,直接全错。 -
只用于非负权图
存在负权边时 Dijkstra 失效,必须用 Bellman-Ford 或 SPFA。 -
忘记把 d[s] 设成 0
这是起点到自己距离,漏掉的话所有点的距离都算不出来。 -
有向图 vs 无向图
本题是有向图,只需要存一条边。无向图要正反都存。
总结
- Dijkstra 是解决非负权图单源最短路的标配算法。
- 核心是“贪心找最近点 + 松弛”,用堆优化可以做到 O((n+m) log n)。
- 模板里的
if(dis!=d[x]) continue;是堆优化的精髓,一定不能省。 - 手动模拟,理解每一行代码的意思比只背模板有用多了,否则一变化就不会了

1017

被折叠的 条评论
为什么被折叠?



