【算法磨剑:用 C++ 思考的艺术・Dijkstra 实战】弱化版 vs 标准版模板,洛谷 P3371/P4779 双题精讲
文章目录
- 前言:
- 《算法磨剑: 用C++思考的艺术》 专栏
- 《C++:从代码到机器》 专栏
- 《Linux系统探幽:从入门到内核》 专栏
- 正文:
- 1:[P3371 【模板】单源最短路径(弱化版](https://www.luogu.com.cn/problem/P3371)
- 【解释】:
- 【代码实现】:(大家会发现,这个代码⾮常像 prim 算法)
- 2.堆优化版 dijkstra 算法
- 【解释】:
- [代码实现]:(其实 prim 算法也有⼀个堆优化的版本,但是时间复杂度和 kk 算法相差⽆⼏,因此没有讲解
- 结语:
前言:
《算法磨剑: 用C++思考的艺术》 专栏
专注用C++破解算法面试真题,详解LeetCode热题,构建算法思维,从容应对技术挑战。
👉 点击关注专栏
《C++:从代码到机器》 专栏
深入探索C++从语法特性至底层原理的奥秘,构建坚实而深入的系统编程知识体系。
👉 点击关注专栏
《Linux系统探幽:从入门到内核》 专栏
深入Linux操作系统腹地,从命令、脚本到系统编程,探索Linux的运作奥秘。
👉 点击关注专栏
作者:孤廖
学习方向:C++/Linux/算法
人生格言:折而不挠,中不为下
正文:
1:P3371 【模板】单源最短路径(弱化版
【解释】:
常规版 dijkstra 算法
Dijkstra 算法是基于贪⼼思想的单源最短路算法,求解的是"⾮负权图"上单源最短路径。

算法磨剑专栏里的 最小生成树中所提及
点击这里跳转
#define _CRT_SECURE_NO_WARNINGS
//P3371 【模板】单源最短路径(弱化版) (模板)
//时间复杂度 o(n^2+m)即o(n^2)#include <iostream>
#include <vector>
using namespace std;
const int N = 1e4 + 10,INF= 2147483647;
typedef pair<int,int> PII;
int n, m, s;//点的个数,边的个数,起源点数
vector<PII> edges[N];//edges[i]:与i点为起点 出边的信息
int dist[N];//dist[i]:起源点到点i的最短路径
bool st[N];//st[i]:点i是否确定起源点到该点的最短路径
void dijkstra()
{//初始化for (int i = 0; i <= n; i++) dist[i] = INF;dist[s] = 0;//每次从没确定最短路径的点中找到最小的路径点即认为起源点到该点//路径已经确定为最小(贪心)//贪心解释:从未确定点中找到的最小点为什么就可以确定起源点到该点的路径最小?//因为其他未确定且能和最小点有路径可能的这些点,本身起源点到这些未确定的点//的路径就已经大于起源点到最小点的路径了 ,所以这些未确定的点没有更小的路径使得//最小点到 起源点的路径大小做出改变(所以这种算法的贪心思想不能用于边权有负数的有向图)for (int i = 1; i < n; i++)//确定n-1个点即n个点全部确定完成{int a = 0;//最小点的下标for (int j = 1; j <= n; j++){if (!st[j] && dist[j] < dist[a])a = j;}//标记最小点 并进行松弛操作st[a] = true;for (auto& e : edges[a]){int b = e.first;//出边接受点int c = e.second;//a~b的边权//更新出边接受点可能的最小路径长度dist[b] = min(dist[b], dist[a] + c);}}//输出结果 遍历dist 数组for (int i = 1; i <= n; i++) cout << dist[i] << " ";}
int main()
{cin >> n >> m >> s;//输入边的信息for (int i = 1; i <= m; i++){int u, v, z;//u->v 的边权zcin >> u >> v >> z;edges[u].push_back({ v,z });}dijkstra();return 0;
}
时间复杂度为o(n^2)
本算法以及后面几篇关于单源最短路径的算法 关于贪心思想的证明 均不做阐述,感兴趣的可以看下《算法导论》这本书
2.堆优化版 dijkstra 算法
【解释】:
堆优化版的 dijkstra 算法流程
- 准备⼯作:
- 创建⼀个⻓度为 n 的 dist 数组,其中 dist[i] 表⽰从起点到 i 结点的最短路;
- 创建⼀个⻓度为 n 的 bool 数组 st ,其中 st[i] 表⽰ i 点是否已经确定了最短路;
- 创建⼀个⼩根堆,维护更新后的结点。(也就是需要确定最短路的结点)
- 初始化: dist[1] = 0 ,然后将 {0, s} 加到堆⾥;其余结点的 dist 值为⽆穷⼤,表⽰还没有找到最短路。
{0,s}即{距离,点} 为什么这么设计? 因为greater这个容器比较的是一个参数
- 重复:弹出堆顶元素,如果该元素已经标记过,就跳过;如果没有标记过,打上标记,进⾏松弛操作。
- 重复上述操作,直到堆⾥⾯没有元素为⽌。
[代码实现]:(其实 prim 算法也有⼀个堆优化的版本,但是时间复杂度和 kk 算法相差⽆⼏,因此没有讲解
感兴趣的可以自行实现
#define _CRT_SECURE_NO_WARNINGS
//【模板】单源最短路径(标准版)#include <iostream>
#include <queue>
#include <vector>
#include <cstring>
using namespace std;
const int N = 1e5 + 10;
typedef pair<int, int> PII;
int n, m, s;
vector<PII> edges[N];
priority_queue<PII, vector<PII>, greater<PII>> heap;//小根堆{距离,节点}
int dist[N];
bool st[N];
void dijkstra()
{//初始化memset(dist, 0x3f3f3f, sizeof dist);dist[s] = 0;heap.push({ 0,s });//从未确定的点中找到最小的点while (heap.size()){int a = heap.top().second;//最小点heap.pop();if (st[a]) continue;//避免拿到确定点之前的较大路径的情况st[a] = true;//将点a的出边进行松弛操作for (auto& e : edges[a]){int b = e.first;int z = e.second;if (dist[a] + z < dist[b]){dist[b] = dist[a] + z;heap.push({ dist[b],b });}}}//输出结果for (int i = 1; i <= n; i++) cout << dist[i] << " ";
}
int main()
{cin >> n >> m >> s;//输入边的信息for (int i = 1; i <= m; i++){int u, v, z;cin >> u >> v >> z;edges[u].push_back({ v,z });}dijkstra();return 0;
}
结语:
这篇 “算法磨剑:用 C++ 思考的艺术”,我们把 Dijkstra 的 “弱化版(朴素)”和“标准版(堆优化)”模板在洛谷双题里 “磨” 透了 ——P3371 用 “三重循环 + 暴力找最小距离”,让你看清算法的 “原始逻辑骨架”;P4779 靠 C++ 的priority_queue实现堆优化,把时间复杂度从(O(n^2))直接压到(O(m\log n)),完美适配 “大规模稀疏图” 的实战场景。两种模板的核心差异,本质是“找「最小距离节点」的效率迭代”,而 C++ 的 STL 容器(vector建邻接表、priority_queue维护候选节点),则是让 “思路” 落地为 “高效代码” 的关键工具
不过,Dijkstra 并非 “单源最短路” 的全部答案 —— 它天生解决不了含负权边的图,更没法直接检测 “负环”(能无限缩短路径的环)。那遇到负权边怎么办?如何高效判断图里有没有负环?明天的专栏,会带来Bellman-Ford 算法、SPFA 算法,还有BF 算法判断负环的技巧,它们能补上 Dijkstra 的 “短板”,覆盖更复杂的图论场景
如果今天的 Dijkstra 双模板解析,帮你理清了 “不同规模图下的实现选择”,不妨点个赞 + 收藏,方便后续对比复习;也欢迎在评论区交流:你用朴素 Dijkstra 时,有没有被 “三重循环” 搞晕过?堆优化的priority_queue在 C++ 里,有没有踩过 “类型匹配 / 自定义比较” 的坑?带着对 “负权边、负环” 的好奇,我们明天继续 “磨” 单源最短路的其他算法,看看它们如何和 Dijkstra 形成 “算法全家桶”~