从朴素到优雅:Dijkstra 算法的两种实现详解
本文基于一道经典最短路题目,对比 朴素 Dijkstra 与 堆优化 Dijkstra 的实现方式、易错点与适用场景,适合算法初学者系统理解。
一、问题背景
给定一个含 nnn 个节点、mmm 条边的无向图 ,求从起点 xxx 到终点 yyy 的最短距离。
数据范围:
- n≤10n \leq 10n≤10
- m≤n(n−1)2m \leq \frac{n(n-1)}{2}m≤2n(n−1)
- 边权 1≤l≤201 \leq l \leq 201≤l≤20
👉 这是一个典型单源最短路径问题,非常适合用来对比两种 Dijkstra 写法。
二、朴素 Dijkstra:小数据的最佳选择
1️⃣ 算法思想
朴素 Dijkstra 的核心是贪心 + 暴力扫描:
- 每次从「未确定最短路径的点」中选出距离起点最近的点
- 用该点去松弛其他所有点
- 重复 nnn 次
时间复杂度:
O(n2) O(n^2) O(n2)
2️⃣ 代码实现(修正版)
cpp
#include <cstring>
#include <iostream>
using namespace std;
const int N = 25;
const int INF = 0x3f3f3f3f;
int g[N][N]; // 邻接矩阵
int dist[N]; // 最短距离
bool vis[N]; // 是否已确定最短路
int n, m, x, y;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
while (cin >> n >> m) {
// 初始化
memset(g, 0x3f, sizeof g);
memset(dist, 0x3f, sizeof dist);
memset(vis, 0, sizeof vis);
for (int i = 1; i <= n; i++) g[i][i] = 0;
for (int i = 1; i <= m; i++) {
int a, b, c;
cin >> a >> b >> c;
g[a][b] = g[b][a] = min(g[a][b], c);
}
cin >> x >> y;
dist[x] = 0;
// Dijkstra
for (int i = 1; i <= n; i++) {
int u = -1, minn = INF;
for (int j = 1; j <= n; j++) {
if (!vis[j] && dist[j] < minn) {
minn = dist[j];
u = j;
}
}
if (u == -1) break; // 图不连通
vis[u] = true;
for (int v = 1; v <= n; v++) {
if (dist[v] > dist[u] + g[u][v]) {
dist[v] = dist[u] + g[u][v];
}
}
}
if (dist[y] == INF)
cout << "No path\n";
else
cout << dist[y] << "\n";
}
return 0;
}
3️⃣ 常见错误(初学者必踩)
| 错误 | 后果 |
|---|---|
| 邻接矩阵未初始化 | 旧数据污染,WA |
| 第一次循环未保证选到起点 | 未定义行为 |
用 g[u][v] != 0 判断边 |
逻辑不严谨 |
| 忽略多组输入 | TLE / WA |
✅ 朴素版适合:n ≤ 500
三、堆优化 Dijkstra:大数据的标准解法
1️⃣ 为什么需要堆优化?
当 nnn 达到 10510^5105 时:
- 朴素版:O(n2)O(n^2)O(n2) → 超时
- 堆优化:O((n+m)logn)O((n+m)\log n)O((n+m)logn) → 可行
核心优化点:
用优先队列快速找到"当前距离最小的点"
2️⃣ 算法思想
- 使用小根堆 维护
(距离, 节点) - 每次取出堆顶作为当前最短点
- 用该点松弛相邻节点,并将更新后的结果重新入堆
3️⃣ 代码实现(链式前向星 + 优先队列)
cpp
#include <cstring>
#include <iostream>
#include <queue>
using namespace std;
typedef pair<int, int> pii;
const int N = 510;
const int M = N * N;
const int INF = 0x3f3f3f3f;
int h[N], e[M], ne[M], w[M], idx;
int dist[N];
bool vis[N];
int n, m, x, y;
priority_queue<pii, vector<pii>, greater<pii>> heap;
void add(int a, int b, int c) {
e[idx] = b;
ne[idx] = h[a];
w[idx] = c;
h[a] = idx++;
}
int dijkstra(int s, int t) {
memset(dist, 0x3f, sizeof dist);
memset(vis, 0, sizeof vis);
dist[s] = 0;
heap.push({0, s});
while (!heap.empty()) {
auto t = heap.top();
heap.pop();
int u = t.second;
if (vis[u]) continue; // 已确定最短路
vis[u] = true;
for (int i = h[u]; i != -1; i = ne[i]) {
int v = e[i];
if (dist[v] > dist[u] + w[i]) {
dist[v] = dist[u] + w[i];
heap.push({dist[v], v});
}
}
}
return dist[t];
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
while (cin >> n >> m) {
memset(h, -1, sizeof h);
idx = 0;
for (int i = 0; i < m; i++) {
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
add(b, a, c);
}
cin >> x >> y;
int ans = dijkstra(x, y);
if (ans == INF)
cout << "No path\n";
else
cout << ans << "\n";
}
return 0;
}
4️⃣ 为什么同一个点会多次入堆?
- 每次松弛都可能产生一个"更优距离"
- 堆中旧版本会被
vis数组过滤 - 这是正常且必要的设计
四、两种写法对比总结
| 对比项 | 朴素 Dijkstra | 堆优化 Dijkstra |
|---|---|---|
| 存图方式 | 邻接矩阵 | 邻接表 / 链式前向星 |
| 选最小点 | 暴力扫描 | 优先队列 |
| 时间复杂度 | O(n2)O(n^2)O(n2) | O((n+m)logn)O((n+m)\log n)O((n+m)logn) |
| 适用场景 | n≤500n \leq 500n≤500 | n≥105n \geq 10^5n≥105 |
| 代码难度 | ⭐ | ⭐⭐⭐ |
| 教学价值 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
五、学习建议(写给初学者)
✅ 不要一上来就写堆优化
- 先写朴素版,理解贪心本质
- 再写堆优化,理解"为什么需要堆"
✅ 算法选型比算法本身更重要
- 小数据:简单算法更稳定
- 大数据:复杂算法才有效
六、一句话总结
朴素 Dijkstra 让你"懂原理",堆优化 Dijkstra 让你"过题目"。
两者缺一不可,是每一位算法学习者必经的成长路径。
如果你喜欢这篇博客,欢迎点赞、收藏、关注 😊