Dijkstra 算法入门笔记 (适用于算法竞赛初学者) - C++ 代码版
目录
- 算法是做什么的?
 - 核心思想:贪就完事了!
 - 算法前提:不能有负权边!
 - 需要哪些工具?(数据结构)
 - 算法具体步骤
 - 关键操作:松弛 (Relaxation)
 - 两种实现方式 (C++ 代码) 
- 朴素版 Dijkstra (O(V^2))
 - 堆优化版 Dijkstra (O(E log V))【竞赛常用】
 
 - 复杂度分析
 - 注意事项与常见问题
 - 练习建议
 - 总结
 
1. 算法是做什么的?
Dijkstra (迪杰斯特拉) 算法主要用于解决 单源最短路径 问题。
- 单源 (Single Source): 指的是从图中的 一个 指定的起始点(源点)出发。
 - 最短路径 (Shortest Path): 找到从这个源点到图中 所有 其他可达点的路径中,权重(或距离、成本)之和最小的那条路径。
 - 图 (Graph): 算法作用于带权重的有向图或无向图。
 
简单说: 给你一张地图(图),上面有各个地点(顶点)以及地点之间的道路长度(边的权重),Dijkstra 算法能帮你快速找到从你的出发点到其他所有地方的最短路线。
2. 核心思想:贪就完事了!
Dijkstra 算法的核心思想是 贪心策略。
- 初始化: 选定一个源点 
s。我们用一个数组dist[v]记录源点s到顶点v的 当前已知最短距离。一开始,dist[s] = 0,其他所有dist[v] = ∞(表示暂时不可达)。 - 迭代: 每次从 还没有确定最终最短路径 的顶点中,选择一个 
dist值最小的顶点u。 - 确定与扩展: 这个 
u的dist[u]值就是源点s到u的 最终最短距离 (因为没有负权边,后面不可能有更短的路绕回来了)。然后,我们利用u来 更新 (或者叫 松弛) 与它相邻的顶点的dist值。如果通过u到达其邻居v的路径 (dist[u] + weight(u, v)) 比当前已知的dist[v]更短,就更新dist[v]。 - 重复: 重复步骤 2 和 3,直到所有顶点都被选中(或者所有可达顶点都被选中)。
 
形象比喻: 想象源点像一个火源开始燃烧,火势(最短路径)总是先蔓延到最近的地方,然后从这些已燃烧的地方继续向外蔓延。
3. 算法前提:不能有负权边!
极其重要: Dijkstra 算法 不能 正确处理带有 负权重边 的图。
- 原因: 算法的贪心策略基于一个假设:一旦一个顶点的最短路径被确定,它就是最终的了。但如果存在负权边,后面可能通过一个负权边“绕回来”,使得已确定“最短路径”的点有了更短的路径,这就破坏了贪心选择的基础。
 - 如果遇到负权边怎么办? 需要使用其他算法,例如 Bellman-Ford 算法或 SPFA 算法 (SPFA 在某些情况下可能被特殊数据卡住,Bellman-Ford 更稳健但效率较低)。
 
4. 需要哪些工具?(数据结构)
实现 Dijkstra 算法通常需要以下数据结构:
- 图的表示: 
- 邻接矩阵 (Adjacency Matrix): 
g[i][j]存储从顶点i到顶点j的边的权重。简单直观,但对于稀疏图(边数远小于点数的平方)空间浪费大。 - 邻接表 (Adjacency List): 
vector<pair<int, int>> adj[N]或类似的结构,adj[u]存储从u出发的所有边,每条边表示为{v, weight}(到达顶点v,权重为weight)。竞赛中最常用,节省空间。 
 - 邻接矩阵 (Adjacency Matrix): 
 - 距离数组 
dist[N]:dist[i]存储源点到顶点i的当前最短距离。初始化为无穷大(一个足够大的数,如0x3f3f3f3f在 C++ 中常用,防止加法溢出),源点dist[source] = 0。 注意使用long long防止溢出! - 标记数组 
visited[N]或st[N](状态 state):visited[i]标记顶点i是否已经找到了最终的最短路径(即是否已经被选为上文步骤 2 中的u)。初始化为false。 (朴素版必需,堆优化版可选) - (堆优化版需要) 优先队列 (Priority Queue): 用于快速找到 
dist值最小的未访问顶点。通常存储自定义结构体State{node, dist}或pair<long long, int>,表示{distance, vertex}。注意 C++std::priority_queue默认是大顶堆,我们需要小顶堆(存储负距离或者自定义比较器,或使用std::greater)。 
5. 算法具体步骤
以源点 s 为例:
- 初始化: 
dist数组所有元素设为无穷大 (LINF)。dist[s] = 0。visited数组所有元素设为false(如果使用)。- (堆优化版) 将 
{s, 0}(节点, 距离) 压入优先队列pq。 
 - 循环 (朴素版 V 次,堆优化版直到队列为空): 
- a. 找到下一个顶点 
u:- 朴素版: 在所有 
visited[i] == false的顶点i中,找到dist[i]最小的那个顶点u。 - 堆优化版: 从优先队列 
pq中取出堆顶元素{u, d}(当前距离d最小的顶点u)。检查d > dist[u],如果是,则跳过 (旧状态)。 
 - 朴素版: 在所有 
 - b. 标记 (可选): 将 
visited[u]设为true(朴素版必需)。 - c. 松弛 
u的邻居: 遍历所有从u出发的边(u, v),其权重为w:- 如果 
dist[u] + w < dist[v],那么:- 更新 
dist[v] = dist[u] + w。 - (堆优化版) 将新的、更短的距离信息 
{v, dist[v]}压入优先队列pq。 
 - 更新 
 
 - 如果 
 
 - a. 找到下一个顶点 
 - 结束: 循环结束后,
dist数组中就存储了源点s到所有可达顶点的最短距离。如果某个dist[i]仍然是LINF,表示从s无法到达i。 
6. 关键操作:松弛 (Relaxation)
松弛是 Dijkstra 算法的核心步骤之一。它的意思是:对于边 (u, v),我们检查是否可以通过顶点 u 来 改善 (缩短) 到达顶点 v 的路径。
// 假设 u 已经被确定了最短路径 dist[u] (或至少是当前最优)
// v 是 u 的一个邻居,边的权重是 weight
// dist 是存储最短距离估计值的数组 (类型通常为 long long)
// LINF 是表示无穷大的常量if (dist[u] != LINF && dist[u] + weight < dist[v]) {dist[v] = dist[u] + weight; // 更新 v 的最短距离估计值// 如果是堆优化版本,还需要将更新后的状态放入优先队列// pq.push({v, dist[v]}); // (伪代码,具体看下方实现)
}
 
7. 两种实现方式 (C++ 代码)
(假设已包含头文件 <vector>, <queue>, <cstring>, <functional>, 并定义了 LINF, MAXN 以及 Edge 结构体,并且 adj, dist, visited, V 是可访问的全局变量或通过参数传递)
// --- 需要的前置定义 (示例) ---
#include <vector>
#include <queue>
#include <cstring>
#include <functional> // for std::greaterconst long long LINF = 0x3f3f3f3f3f3f3f3fLL;
const int MAXN = 100005;struct Edge {int to;int weight;
};struct State {int node;long long dist;bool operator>(const State& other) const {return dist > other.dist;}
};extern std::vector<Edge> adj[MAXN]; // 邻接表 (需在外部定义和填充)
extern long long dist[MAXN];        // 距离数组 (需在外部定义)
extern bool visited[MAXN];        // 访问标记 (需在外部定义)
extern int V;                     // 顶点数 (需在外部定义)
// --- 前置定义结束 --- 
朴素版 Dijkstra (O(V^2))
// --- 朴素版 Dijkstra 函数 ---
void dijkstra_simple(int start_node) {// 1. 初始化for (int i = 1; i <= V; ++i) { // 假设顶点从 1 到 Vdist[i] = LINF;visited[i] = false;}dist[start_node] = 0;// 2. 循环 V 次for (int i = 0; i < V; ++i) {int u = -1;long long min_dist = LINF;// a. 找到 dist 最小的未访问顶点 ufor (int j = 1; j <= V; ++j) {if (!visited[j] && dist[j] < min_dist) {min_dist = dist[j];u = j;}}// 如果找不到或剩下的点不可达if (u == -1) {break;}// b. 标记 u 为已访问visited[u] = true;// c. 松弛 u 的邻居for (const auto& edge : adj[u]) {int v = edge.to;int weight = edge.weight;// 确保 dist[u] 不是 LINF 且 v 未访问if (dist[u] != LINF && !visited[v] && dist[u] + weight < dist[v]) {dist[v] = dist[u] + weight;}}}
}
 
堆优化版 Dijkstra (O(E log V))【竞赛常用】
// --- 堆优化版 Dijkstra 函数 ---
void dijkstra_heap(int start_node) {// 1. 初始化for (int i = 1; i <= V; ++i) { // 假设顶点从 1 到 Vdist[i] = LINF;// visited 在堆优化版中通常不是必需的,但有时可用于小幅优化// visited[i] = false;}dist[start_node] = 0;// 优先队列,存储 {节点, 距离},按距离从小到大排序 (小顶堆)std::priority_queue<State, std::vector<State>, std::greater<State>> pq;pq.push({start_node, 0});// 2. 主循环while (!pq.empty()) {// a. 取出当前距离最小的状态State current = pq.top();pq.pop();int u = current.node;long long current_dist = current.dist;// **核心优化**: 如果取出的距离比记录的还大,说明是旧状态,跳过if (current_dist > dist[u]) {continue;}// 如果使用 visited 数组优化:// if (visited[u]) continue;// visited[u] = true;// b. 松弛 u 的邻居for (const auto& edge : adj[u]) {int v = edge.to;int weight = edge.weight;// 确保 dist[u] 不是 LINFif (dist[u] != LINF && dist[u] + weight < dist[v]) {dist[v] = dist[u] + weight;// 将更新后的、更短的距离信息加入优先队列pq.push({v, dist[v]});}}}
}
 
注意 long long: 在竞赛中,路径长度之和很容易超过 int 的最大值 (约 2 * 10^9)。强烈建议使用 long long 来存储 dist 数组,避免溢出导致错误。
8. 复杂度分析
- 朴素版: 
- 时间复杂度:O(V^2)。
 - 空间复杂度:O(V + E) (邻接表) 或 O(V^2) (邻接矩阵)。
 
 - 堆优化版: 
- 时间复杂度:O(E log V)。
 - 空间复杂度:O(V + E)。
 
 
竞赛中,由于 V 通常较大,E 相对 V^2 较小,堆优化版的 O(E log V) 是必须掌握的。
9. 注意事项与常见问题
- 负权边: 再次强调,Dijkstra 不能处理负权边。
 - 图的表示: 竞赛通常用邻接表。
 - 重边和自环: Dijkstra 能正确处理。
 - 图不连通: 
dist数组中不可达顶点的距离将保持为LINF。 - INF 的选择: 
LINF要足够大,0x3f3f3f3f3f3f3f3fLL是常用的long long型无穷大。 - 起点和终点编号: 注意题目是从 0 还是 1 开始。代码示例假设从 1 开始。
 - 路径记录: 如果需要输出路径,可在松弛时记录前驱节点 
parent[v] = u。 
10. 练习建议
- 模板题: 洛谷 P3371 (朴素/堆), P4779 (堆优化), AcWing 849 (朴素), 850 (堆优化)。
 - 练习平台: 洛谷, AcWing, LeetCode, 牛客网。
 - 做题策略: 先实现模板,再做变化题。
 
11. 总结
Dijkstra 算法是图论中基础且重要的单源最短路径算法,核心是 贪心 和 松弛。掌握 堆优化版本 对于算法竞赛至关重要。理解原理、前提、细节,并通过 C++ 代码实践和做题来巩固。
