今天我们一起来学习图论最短路径算法之一的Dijkstra算法!
在图论算法中,最短路径是最核心、最高频的考点之一,而 Dijkstra(迪杰斯特拉)算法 是求解正权图单源最短路径的最优算法。无论是算法竞赛、编程刷题,还是实际项目中的路径规划、导航测距、网络拓扑计算,Dijkstra 算法都有着不可替代的作用。
本文将从零基础原理讲解入手,手把手带大家掌握 C++ 实现的两种版本:适配稠密图的朴素版 Dijkstra、适配大数据稀疏图的堆优化版 Dijkstra。全文搭配完整可运行代码、详细注释、测试样例、易错点解析,内容详实完整,适合新手学习、收藏复盘,也可直接作为博客干货内容留存。
一、算法基础认知与适用场景
1. 核心定义
单源最短路径:给定一张带权图和一个起始节点(源点),求解源点到图中所有其他节点的最短路径长度。
Dijkstra 算法是基于贪心思想 的最短路径算法,核心特性:仅适用于边权非负的图(无负权边)。
2. 优缺点与适用场景
-
优点:效率高、逻辑清晰、稳定性强,正权图下时间复杂度远优于 Bellman-Ford、SPFA 算法。
-
缺点:无法处理负权边、负权环,一旦存在负权边,贪心逻辑失效,计算结果出错。
-
场景区分:朴素版适配节点数较少的稠密图,堆优化版适配大规模稀疏图,是工业级和竞赛级的通用模板。
3. 核心前置概念
松弛操作(算法灵魂):假设已知源点到节点 u 的最短距离为 distu,存在一条边 u→v,边权为 w。若通过 u 中转到达 v 的距离,比当前记录的源点到 v 的距离更短,则更新 v 的最短距离。
公式表达:
简单理解:发现更短的路径,就更新最短距离,所有 Dijkstra 算法的核心逻辑都围绕松弛操作展开。
二、通用变量约定(全文统一)
-
n:图的节点总数 -
m:图的边总数 -
s:起始源点 -
dist[]:距离数组,dist[i]表示源点到 i 号节点的最短距离 -
vis[]:标记数组,记录节点的最短路径是否已确定 -
INF:无穷大,本文统一使用0x3f3f3f3f,数值稳定、不易溢出,是 C++ 图论通用无穷值
三、朴素版 Dijkstra 算法(邻接矩阵 · 新手入门)
1. 原理与复杂度
朴素版采用邻接矩阵存图,核心逻辑:每次遍历所有未确定最短路径的节点,选出距离源点最近的节点,固定其最短路径,再通过该节点松弛所有相邻节点。
时间复杂度:O(n²),适合节点数 ≤ 1000 的稠密图,逻辑直观,是新手理解 Dijkstra 算法的最佳版本。
2. 完整可运行代码(超详细注释)
cpp
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
// 最大节点数
const int N = 505;
// 无穷大常量
const int INF = 0x3f3f3f3f;
int g[N][N]; // 邻接矩阵存图:g[u][v] 表示 u到v 的边权
int dist[N]; // 存储源点到各点的最短距离
bool vis[N]; // 标记节点是否已确定最短路径
int n, m, s; // 节点数、边数、源点
// 朴素Dijkstra核心函数
void Dijkstra()
{
// 初始化所有距离为无穷大
memset(dist, 0x3f, sizeof(dist));
// 源点到自身距离为0
dist[s] = 0;
// 遍历n轮,每轮确定一个节点的最短路径
for (int i = 1; i <= n; i++)
{
// 步骤1:找到未访问、距离最小的节点u
int u = -1;
for (int j = 1; j <= n; j++)
{
if (!vis[j] && (u == -1 || dist[j] < dist[u]))
{
u = j;
}
}
// 标记该节点最短路径已确定
vis[u] = true;
// 步骤2:用节点u松弛所有相邻节点
for (int v = 1; v <= n; v++)
{
// 两点之间存在有效边时,执行松弛操作
if (g[u][v] != INF)
{
dist[v] = min(dist[v], dist[u] + g[u][v]);
}
}
}
}
int main()
{
// 初始化邻接矩阵为无穷大
memset(g, 0x3f, sizeof(g));
cin >> n >> m >> s;
// 读入所有边
for (int i = 1; i <= m; i++)
{
int u, v, w;
cin >> u >> v >> w;
// 处理重边:保留两点间最小边权
g[u][v] = min(g[u][v], w);
}
// 执行最短路算法
Dijkstra();
// 输出源点到所有节点的最短距离
for (int i = 1; i <= n; i++)
{
if (dist[i] == INF)
cout << "INF ";
else
cout << dist[i] << " ";
}
return 0;
}
3. 测试样例与运行结果
输入样例:
5 7 1 1 2 2 1 3 1 2 4 5 3 2 1 3 4 3 3 5 4 4 5 1
输出结果 :0 2 1 4 5
结果释义:源点1到1、2、3、4、5号节点的最短距离分别为 0、2、1、4、5。
四、堆优化 Dijkstra 算法(邻接表 · 竞赛/工程标配)
1. 优化原理
朴素算法的最大短板是:每次遍历全图寻找最短距离节点,造成大量无效遍历。针对这一问题,我们采用**邻接表存图+小根堆(优先队列)**优化。
通过优先队列自动排序,可直接取出当前距离最小的节点,将选点复杂度从 O(n) 降至 O(logn),整体时间复杂度优化为 O(m logn),可处理 1e5 级别的大规模数据,是实际开发和算法竞赛的通用模板。
2. 核心知识点
-
邻接表 :用
vector<pair<int, int>>存储边,仅记录有效边,大幅节省空间,适配稀疏图。 -
小根堆 :默认优先队列是大根堆,通过
greater重载为小根堆,实现最小距离优先出队。 -
冗余节点处理:同一节点会多次入队,只需取出时判断是否已确定最短路,已确定则直接跳过即可。
3. 完整优化版代码
cpp
#include <iostream>
#include <vector>
#include <queue>
#include <cstring>
using namespace std;
// 适配1e5大规模数据
const int N = 100010;
const int INF = 0x3f3f3f3f;
// 邻接表:adj[u] 存储所有 u 出发的边 {终点, 边权}
vector<pair<int, int>> adj[N];
int dist[N];
bool vis[N];
int n, m, s;
void Dijkstra_Heap()
{
memset(dist, 0x3f, sizeof(dist));
dist[s] = 0;
// 小根堆:pair(距离, 节点编号),距离最小优先
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> q;
q.push({0, s});
while (!q.empty())
{
// 取出当前距离最小的节点
auto now = q.top();
q.pop();
int d = now.first;
int u = now.second;
// 跳过已确定最短路的冗余节点
if (vis[u]) continue;
vis[u] = true;
// 遍历所有邻边,执行松弛操作
for (auto edge : adj[u])
{
int v = edge.first;
int w = edge.second;
if (dist[v] > dist[u] + w)
{
dist[v] = dist[u] + w;
q.push({dist[v], v});
}
}
}
}
int main()
{
// 关闭cin同步,大幅加速大数据输入
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m >> s;
for (int i = 1; i <= m; i++)
{
int u, v, w;
cin >> u >> v >> w;
// 有向图建边
adj[u].push_back({v, w});
// 无向图需额外添加:adj[v].push_back({u, w});
}
Dijkstra_Heap();
// 批量输出结果
for (int i = 1; i <= n; i++)
{
if (dist[i] == INF)
cout << "INF ";
else
cout << dist[i] << " ";
}
return 0;
}
五、博客必备:高频易错点总结
这部分是新手最容易踩坑的内容,也是刷题、面试高频考点,建议重点记忆:
-
禁止处理负权边:Dijkstra 依赖贪心思想,一旦存在负权边,已确定的最短路径可能被后续更新,算法直接失效,负权图请使用 SPFA 算法。
-
INF 取值禁忌 :不要使用
INT_MAX,会导致加法溢出报错,0x3f3f3f3f是最安全的无穷值。 -
无向图建边规则 :无向边是双向边,需要两次
push_back建边,否则路径缺失。 -
堆冗余不删除 :优先队列无需手动删除旧数据,判断
vis标记跳过即可,这是最优写法。 -
大数据加速必备 :处理 1e4 以上数据时,必须添加
ios::sync_with_stdio(false);cin.tie(0);,避免输入超时。
六、学习总结
-
朴素版 Dijkstra:逻辑简单、适合入门理解算法原理,仅用于小数据稠密图,日常学习优先先掌握此版本,吃透贪心和松弛核心思想。
-
堆优化 Dijkstra:效率极高、适配大数据,是开发、刷题、面试的万能模板,建议直接背诵留存。
-
算法核心本质:贪心选最短节点 + 松弛更新路径,所有优化方式都是在优化"选节点"的效率,核心逻辑从未改变。