[{"content":" 本文是 ACM 算法训练营 系列第 4 篇。前置阅读：图论基础：图的存储与遍历。\n算法介绍 今天我们来学习 Dijkstra 算法，这是图论里最核心的算法之一。它能解决这样一个问题：给定一个带权有向图，找出从起点到所有其它节点的最短路径。注意，Dijkstra 只能处理边权非负的情况——如果存在负权边，需要用 Bellman-Ford 或 SPFA。\n现实中的导航软件、网络路由协议（OSPF），底层都有 Dijkstra 的影子。ACM 竞赛里，最短路的题目占比非常高，Dijkstra 是最短路入门必学的第一课。\n算法思想 Dijkstra 的核心思想是贪心：每次从\u0026quot;尚未确定最短距离\u0026quot;的节点中，选一个\u0026quot;当前距离最小\u0026quot;的节点，把它标记为已确定，然后用它去更新邻居的距离。这个过程叫松弛（Relaxation）。\n步骤 操作 说明 1 初始化 dist[start] = 0，其余节点 dist = INF 2 选点 从未确定集合中选出 dist 最小的节点 u 3 松弛 对 u 的每条出边 (u, v, w)，若 dist[u] + w \u0026lt; dist[v]，更新 dist[v] 4 标记 将 u 标记为已确定，重复 2-4 直到所有节点确定 朴素实现 — O(V²) 最直接的写法：每一轮暴力扫描所有节点，找 dist 最小的那个。适合稠密图（边数接近 V²）。\n#include \u0026lt;bits/stdc++.h\u0026gt; using namespace std; const int MAXN = 1005; const int INF = 0x3f3f3f3f; int n, m, s; // 点数、边数、起点 int g[MAXN][MAXN]; // 邻接矩阵，g[u][v] = w int dist[MAXN]; bool vis[MAXN]; // 是否已确定 void dijkstra() { memset(dist, 0x3f, sizeof(dist)); dist[s] = 0; for (int i = 1; i \u0026lt;= n; i++) { // 1. 从未确定节点中选出 dist 最小的 int u = -1; for (int j = 1; j \u0026lt;= n; j++) if (!vis[j] \u0026amp;\u0026amp; (u == -1 || dist[j] \u0026lt; dist[u])) u = j; if (dist[u] == INF) break; // 剩下的节点不可达，提前结束 vis[u] = true; // 2. 用 u 松弛所有邻居 for (int v = 1; v \u0026lt;= n; v++) if (g[u][v] != INF) dist[v] = min(dist[v], dist[u] + g[u][v]); } } 堆优化 — O((V+E) log V) 稀疏图时，暴力扫描 O(V²) 太慢。观察步骤 2——\u0026ldquo;找最小值的节点\u0026rdquo;，这正是优先队列的强项。用 priority_queue 把找点的开销从 O(V) 降到 O(log V)。\n#include \u0026lt;bits/stdc++.h\u0026gt; using namespace std; const int MAXN = 100005; const int INF = 0x3f3f3f3f; struct Edge { int to, w; }; vector\u0026lt;Edge\u0026gt; g[MAXN]; // 邻接表 int dist[MAXN]; bool vis[MAXN]; void dijkstra(int s) { memset(dist, 0x3f, sizeof(dist)); dist[s] = 0; // 小根堆：pair\u0026lt;距离, 节点编号\u0026gt; priority_queue\u0026lt;pair\u0026lt;int, int\u0026gt;, vector\u0026lt;pair\u0026lt;int, int\u0026gt;\u0026gt;, greater\u0026lt;\u0026gt;\u0026gt; pq; pq.push({0, s}); while (!pq.empty()) { auto [d, u] = pq.top(); pq.pop(); if (vis[u]) continue; // 过期的旧值，跳过 vis[u] = true; for (auto \u0026amp;e : g[u]) { int v = e.to, w = e.w; if (dist[v] \u0026gt; dist[u] + w) { dist[v] = dist[u] + w; pq.push({dist[v], v}); // 松弛成功后入堆 } } } } 为什么新入堆而不是修改堆中旧值？ C++ priority_queue 不支持 decrease-key 操作。我们直接 push 新值，旧值留在堆底——当它被 pop 时，vis[u] 已经在 true 状态，直接 continue 跳过。这是最常用的写法，简洁且实际效率不差。\n手写二叉堆 — 支持 decrease-key 如果你真的需要 decrease-key（比如某些卡常题目），可以手写一个最小堆：\n// 手写最小堆，支持 decrease-key struct MinHeap { pair\u0026lt;int, int\u0026gt; h[MAXN]; // {dist, node} int pos[MAXN]; // pos[node] = index in heap, 0 表示不在堆中 int sz; void init() { sz = 0; memset(pos, 0, sizeof(pos)); } void swap_node(int i, int j) { swap(h[i], h[j]); pos[h[i].second] = i; pos[h[j].second] = j; } void push(int node, int d) { h[++sz] = {d, node}; pos[node] = sz; int i = sz; while (i \u0026gt; 1 \u0026amp;\u0026amp; h[i].first \u0026lt; h[i / 2].first) { swap_node(i, i / 2); i /= 2; } } void decrease_key(int node, int new_d) { int i = pos[node]; if (i == 0) { push(node, new_d); return; } h[i].first = new_d; while (i \u0026gt; 1 \u0026amp;\u0026amp; h[i].first \u0026lt; h[i / 2].first) { swap_node(i, i / 2); i /= 2; } } pair\u0026lt;int, int\u0026gt; pop() { auto res = h[1]; pos[res.second] = 0; h[1] = h[sz--]; if (sz \u0026gt; 0) { pos[h[1].second] = 1; int i = 1; while (true) { int mn = i; if (i * 2 \u0026lt;= sz \u0026amp;\u0026amp; h[i * 2].first \u0026lt; h[mn].first) mn = i * 2; if (i * 2 + 1 \u0026lt;= sz \u0026amp;\u0026amp; h[i * 2 + 1].first \u0026lt; h[mn].first) mn = i * 2 + 1; if (mn == i) break; swap_node(i, mn); i = mn; } } return res; } }; 常见易错点 把 Dijkstra 用在负权图上 — 结果是错的。Dijkstra 依赖\u0026quot;当前最小距离不会再被更新\u0026quot;这一性质，负权边会破坏它。有负权请用 Bellman-Ford 或 SPFA。 INF 溢出 — dist[u] + w 时若 dist[u] = INF（比如 0x3f3f3f3f），加一个正权会溢出成负数。写法上需要判断 dist[u] != INF 或使用 long long。 vis 标记时机 — vis 应该在出队时标记，而不是入队时。同一个节点可能多次入队（不同距离），只有第一次 pop 的时候才真正确定。 重边处理 — 邻接矩阵写法需要取 min(g[u][v], w)，邻接表则不需要，因为每条边独立存储。 堆中节点被多次 push — 如前所述，这是预期行为，vis 检查即可过滤。 复杂度对比 实现方式 时间复杂度 适用场景 朴素 O(V²) O(V²) 稠密图，V ≤ 1000 STL 堆 O((V+E) log V) 稀疏图，通用首选 手写二叉堆 O((V+E) log V) 需要 decrease-key，常数更小 Fibonacci 堆 O(E + V log V) 理论上最优，竞赛中几乎不用（常数大） 实战题目推荐 P3371 【模板】单源最短路径（弱化版） — 直接套板子 P4779 【模板】单源最短路径（标准版） — 必须用堆优化 P1339 [USACO09OCT] Heat Wave — 简单应用 P1144 最短路计数 — Dijkstra + DP 计数 总结 Dijkstra 算法虽然思想上很直观——贪心地每次取最近节点进行松弛——但真正写对、写好，还是有不少细节需要注意。今天主要掌握三点：邻接表建图 + priority_queue 堆优化 + vis 去重，这是竞赛中最实用的写法。\n感觉 Dijkstra 算法还是有一定难度的，尤其是手写堆部分和负权边边界条件。下次继续学习 Bellman-Ford 和 SPFA，把最短路这一块彻底吃透。\n","permalink":"https://blog.codequest.com.cn/posts/series-acm/04-zui-duan-lu-suan-fa/","summary":"从原理到实现，深入理解 Dijkstra 最短路径算法。含朴素版、堆优化版、手写二叉堆版三种实现，配合图例与常见陷阱分析。","title":"最短路算法：Dijkstra 算法详解"},{"content":"为什么写这篇笔记 加入 ACM 集训队后，我逐渐意识到算法训练不是刷题数量的堆砌，而是对问题本质的理解与模式识别能力的积累。这篇笔记记录我的训练路径和踩过的坑。\n训练路线 第一阶段：基础数据结构 数组、链表、栈、队列 哈希表、集合 二叉树、堆 第二阶段：经典算法 动态规划\n动态规划的核心是状态定义与状态转移。常见类型：\n// 01 背包 int dp[1005][1005]; for (int i = 1; i \u0026lt;= n; i++) { for (int j = 0; j \u0026lt;= W; j++) { dp[i][j] = dp[i-1][j]; if (j \u0026gt;= w[i]) dp[i][j] = max(dp[i][j], dp[i-1][j-w[i]] + v[i]); } } 图论\n算法 时间复杂度 适用场景 Dijkstra O((V+E)logV) 单源最短路，非负权 Bellman-Ford O(VE) 可检测负环 Floyd O(V³) 全源最短路 SPFA 平均 O(E) 稀疏图优化 第三阶段：高级算法 线段树与树状数组 网络流 字符串算法（KMP、后缀数组） 心得 不要只抄题解 — 先独立思考 30 分钟 建立错题本 — 记录 WA 的原因和思维盲区 定期复盘 — 每周回顾做过的题目，总结模式 下一步 继续深入字符串算法和计算几何，准备区域赛。\n","permalink":"https://blog.codequest.com.cn/posts/acm-suan-fa-xun-lian-bi-ji/","summary":"记录 ACM 集训队训练中的算法学习路径与实战心得，涵盖动态规划、图论与字符串算法。","title":"ACM 算法训练笔记：从入门到实战"},{"content":"我是 Eon，华南理工大学计算机科学与技术专业学生（2025级），ACM 集训队成员。\n我信奉**「项目驱动、实战检验、持续迭代」**——以代码解决实际问题，致力于成为优秀的系统级与算法研发工程师。\n🏫 教育与成长 教育背景 华南理工大学\n计算机科学与技术 · 本科 (2025 - 2029)\n大一在读。系统学习算法设计、体系结构，主攻 C++ 系统并发与硬件描述语言 (Verilog)。\n目标与路线 当前研究重点\n深耕数据结构与高级算法设计（线段树、图论、DP） 加强 C++ 硬件协同与底层系统高并发实践 推进 LoongArch 架构 CPU 设计与 FPGA 仿真开发 ⚡ 技能矩阵 (Skills) 📐 算法竞赛 动态规划 图论算法 高级数据结构 字符串 (KMP) ACM 集训 Codeforces ⚙️ 系统开发 C++ C Python Linux CMake Git GCC/GDB 🔌 硬件底层 Verilog FPGA 开发 Vivado 仿真 LoongArch CPU 体系结构 ","permalink":"https://blog.codequest.com.cn/about/","summary":"\u003cp\u003e我是 \u003cstrong\u003eEon\u003c/strong\u003e，华南理工大学计算机科学与技术专业学生（2025级），ACM 集训队成员。\u003c/p\u003e\n\u003cp\u003e我信奉**「项目驱动、实战检验、持续迭代」**——以代码解决实际问题，致力于成为优秀的系统级与算法研发工程师。\u003c/p\u003e","title":"关于"},{"content":"欢迎留言交换友链！请在下方评论区提交您的信息，我将尽快添加。\n🔗 申请格式 名称：Eon's Blog 链接：https://blog.codequest.com.cn 头像：https://blog.codequest.com.cn/favicon.ico 描述：华南理工大学计算机专业学生，ACM 集训队成员，专注算法与系统开发。 ","permalink":"https://blog.codequest.com.cn/friends/","summary":"friends","title":"友情链接"}]