当前位置: 首页 > news >正文

Dijkstra 算法入门笔记 (适用于算法竞赛初学者) - C++ 代码版

目录

  1. 算法是做什么的?
  2. 核心思想:贪就完事了!
  3. 算法前提:不能有负权边!
  4. 需要哪些工具?(数据结构)
  5. 算法具体步骤
  6. 关键操作:松弛 (Relaxation)
  7. 两种实现方式 (C++ 代码)
    • 朴素版 Dijkstra (O(V^2))
    • 堆优化版 Dijkstra (O(E log V))【竞赛常用】
  8. 复杂度分析
  9. 注意事项与常见问题
  10. 练习建议
  11. 总结

1. 算法是做什么的?

Dijkstra (迪杰斯特拉) 算法主要用于解决 单源最短路径 问题。

  • 单源 (Single Source): 指的是从图中的 一个 指定的起始点(源点)出发。
  • 最短路径 (Shortest Path): 找到从这个源点到图中 所有 其他可达点的路径中,权重(或距离、成本)之和最小的那条路径。
  • 图 (Graph): 算法作用于带权重的有向图或无向图。

简单说: 给你一张地图(图),上面有各个地点(顶点)以及地点之间的道路长度(边的权重),Dijkstra 算法能帮你快速找到从你的出发点到其他所有地方的最短路线。

2. 核心思想:贪就完事了!

Dijkstra 算法的核心思想是 贪心策略

  1. 初始化: 选定一个源点 s。我们用一个数组 dist[v] 记录源点 s 到顶点 v当前已知最短距离。一开始,dist[s] = 0,其他所有 dist[v] = ∞ (表示暂时不可达)。
  2. 迭代: 每次从 还没有确定最终最短路径 的顶点中,选择一个 dist 值最小的顶点 u
  3. 确定与扩展: 这个 udist[u] 值就是源点 su最终最短距离 (因为没有负权边,后面不可能有更短的路绕回来了)。然后,我们利用 u更新 (或者叫 松弛) 与它相邻的顶点的 dist 值。如果通过 u 到达其邻居 v 的路径 (dist[u] + weight(u, v)) 比当前已知的 dist[v] 更短,就更新 dist[v]
  4. 重复: 重复步骤 2 和 3,直到所有顶点都被选中(或者所有可达顶点都被选中)。

形象比喻: 想象源点像一个火源开始燃烧,火势(最短路径)总是先蔓延到最近的地方,然后从这些已燃烧的地方继续向外蔓延。

3. 算法前提:不能有负权边!

极其重要: Dijkstra 算法 不能 正确处理带有 负权重边 的图。

  • 原因: 算法的贪心策略基于一个假设:一旦一个顶点的最短路径被确定,它就是最终的了。但如果存在负权边,后面可能通过一个负权边“绕回来”,使得已确定“最短路径”的点有了更短的路径,这就破坏了贪心选择的基础。
  • 如果遇到负权边怎么办? 需要使用其他算法,例如 Bellman-Ford 算法或 SPFA 算法 (SPFA 在某些情况下可能被特殊数据卡住,Bellman-Ford 更稳健但效率较低)。

4. 需要哪些工具?(数据结构)

实现 Dijkstra 算法通常需要以下数据结构:

  1. 图的表示:
    • 邻接矩阵 (Adjacency Matrix): g[i][j] 存储从顶点 i 到顶点 j 的边的权重。简单直观,但对于稀疏图(边数远小于点数的平方)空间浪费大。
    • 邻接表 (Adjacency List): vector<pair<int, int>> adj[N] 或类似的结构,adj[u] 存储从 u 出发的所有边,每条边表示为 {v, weight} (到达顶点 v,权重为 weight)。竞赛中最常用,节省空间。
  2. 距离数组 dist[N]: dist[i] 存储源点到顶点 i 的当前最短距离。初始化为无穷大(一个足够大的数,如 0x3f3f3f3f 在 C++ 中常用,防止加法溢出),源点 dist[source] = 0注意使用 long long 防止溢出!
  3. 标记数组 visited[N]st[N] (状态 state): visited[i] 标记顶点 i 是否已经找到了最终的最短路径(即是否已经被选为上文步骤 2 中的 u)。初始化为 false。 (朴素版必需,堆优化版可选)
  4. (堆优化版需要) 优先队列 (Priority Queue): 用于快速找到 dist 值最小的未访问顶点。通常存储自定义结构体 State{node, dist}pair<long long, int>,表示 {distance, vertex}。注意 C++ std::priority_queue 默认是大顶堆,我们需要小顶堆(存储负距离或者自定义比较器,或使用 std::greater)。

5. 算法具体步骤

以源点 s 为例:

  1. 初始化:
    • dist 数组所有元素设为无穷大 (LINF)。
    • dist[s] = 0
    • visited 数组所有元素设为 false (如果使用)。
    • (堆优化版) 将 {s, 0} (节点, 距离) 压入优先队列 pq
  2. 循环 (朴素版 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
  3. 结束: 循环结束后,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. 注意事项与常见问题

  1. 负权边: 再次强调,Dijkstra 不能处理负权边。
  2. 图的表示: 竞赛通常用邻接表。
  3. 重边和自环: Dijkstra 能正确处理。
  4. 图不连通: dist 数组中不可达顶点的距离将保持为 LINF
  5. INF 的选择: LINF 要足够大,0x3f3f3f3f3f3f3f3fLL 是常用的 long long 型无穷大。
  6. 起点和终点编号: 注意题目是从 0 还是 1 开始。代码示例假设从 1 开始。
  7. 路径记录: 如果需要输出路径,可在松弛时记录前驱节点 parent[v] = u

10. 练习建议

  • 模板题: 洛谷 P3371 (朴素/堆), P4779 (堆优化), AcWing 849 (朴素), 850 (堆优化)。
  • 练习平台: 洛谷, AcWing, LeetCode, 牛客网。
  • 做题策略: 先实现模板,再做变化题。

11. 总结

Dijkstra 算法是图论中基础且重要的单源最短路径算法,核心是 贪心松弛。掌握 堆优化版本 对于算法竞赛至关重要。理解原理、前提、细节,并通过 C++ 代码实践和做题来巩固。

相关文章:

  • 【上位机——MFC】消息映射机制
  • AI日报 - 2025年04月21日
  • SQL之DML(查询语句:select、where)
  • 数据通信学习笔记之OSPF的区域
  • AIGC赋能插画创作:技术解析与代码实战详解
  • 自由的控件开发平台:飞帆中使用 css 和 js 库
  • LeetCode283.移动零
  • HTTP 1.0 和 2.0 的区别
  • 阿拉丁神灯-第16届蓝桥第4次STEMA测评Scratch真题第2题
  • Redis 缓存—处理高并发问题
  • 对于网络资源二级缓存的简单学习
  • 【嵌入式人工智能产品开发实战】(二十一)—— 政安晨:源码搭建小智AI嵌入式终端的后端服务(服务器)环境 - 助力嵌入式人工智能开发
  • 测试基础笔记第七天
  • [FPGA]设计一个DDS信号发生器
  • 每天学一个 Linux 命令(28):ln
  • CentOS stream 中部署Zabbix RPM软件包公钥验证错误
  • 20.3 使用技巧6
  • 自定义 strlen 函数:递归实现字符串长度计算
  • 如何使用人工智能大模型,免费快速写工作计划?
  • kotlin,编码、解码
  • 人民日报钟声:世界决不能重回弱肉强食的时代
  • “棉花糖爸爸”陈生梨:女儿将落户到贵州纳雍
  • ETF市场规模首破4万亿,月内ETF基金净流入超3000亿
  • 央视网评论员:婚约不是性许可——山西订婚强奸案背后的性教育盲区
  • 人民日报读者点题·共同关注:今天,我们需要什么样的企业家?
  • 吉林省文联党组书记、主席赵明接受纪律审查和监察调查