【算法磨剑:用 C++ 思考的艺术・Dijkstra 实战】弱化版 vs 标准版模板,洛谷 P3371/P4779 双题精讲


文章目录

  • 前言:
    • [《算法磨剑: 用C++思考的艺术》 专栏](#《算法磨剑: 用C++思考的艺术》 专栏)
    • [《C++:从代码到机器》 专栏](#《C++:从代码到机器》 专栏)
    • [《Linux系统探幽:从入门到内核》 专栏](#《Linux系统探幽:从入门到内核》 专栏)
  • 正文:
    • [1:[P3371 【模板】单源最短路径(弱化版](https://www.luogu.com.cn/problem/P3371)](#1:P3371 【模板】单源最短路径(弱化版)
      • 【解释】:
      • [【代码实现】:(大家会发现,这个代码⾮常像 prim 算法)](#【代码实现】:(大家会发现,这个代码⾮常像 prim 算法))
    • [2.堆优化版 dijkstra 算法](#2.堆优化版 dijkstra 算法)
      • 【解释】:
      • [[代码实现]:(其实 prim 算法也有⼀个堆优化的版本,但是时间复杂度和 kk 算法相差⽆⼏,因此没有讲解](#[代码实现]:(其实 prim 算法也有⼀个堆优化的版本,但是时间复杂度和 kk 算法相差⽆⼏,因此没有讲解)
  • 结语:

前言:

《算法磨剑: 用C++思考的艺术》 专栏

专注用C++破解算法面试真题,详解LeetCode热题,构建算法思维,从容应对技术挑战。

👉 点击关注专栏


《C++:从代码到机器》 专栏

深入探索C++从语法特性至底层原理的奥秘,构建坚实而深入的系统编程知识体系。

👉 点击关注专栏


《Linux系统探幽:从入门到内核》 专栏

深入Linux操作系统腹地,从命令、脚本到系统编程,探索Linux的运作奥秘。

👉 点击关注专栏


作者:孤廖
学习方向 :C++/Linux/算法
人生格言:折而不挠,中不为下

正文:

1:P3371 【模板】单源最短路径(弱化版

【解释】:

常规版 dijkstra 算法

Dijkstra 算法是基于贪⼼思想的单源最短路算法,求解的是"⾮负权图"上单源最短路径。

![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/983e9a93ab69442994f4cd4454995e40.png
常规版 dijkstra 算法流程:

  • 准备⼯作:
  • 创建⼀个⻓度为 n 的 dist 数组,其中 dist[i] 表⽰从起点到 i 结点的最短路;
  • 创建⼀个⻓度为 n 的 bool 数组 st ,其中 st[i] 表⽰ i 点是否已经确定了最短路。

• 初始化: dist[1] = 0 ,其余结点的 dist 值为⽆穷⼤,表⽰还没有找到最短路。

  • 重复:在所有没有确定最短路的点中,找出最短路⻓度最⼩的点 u 。打上确定最短路的标记,然后对 u 的出边进⾏松弛操作
  • 重复上述操作,直到所有点的最短路都确定

【代码实现】:(大家会发现,这个代码⾮常像 prim 算法)

算法磨剑专栏里的 最小生成树中所提及
点击这里跳转

复制代码
#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 的边权z
		cin >> 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 形成 "算法全家桶"~

相关推荐
程序员东岸7 分钟前
《数据结构——排序(中)》选择与交换的艺术:从直接选择到堆排序的性能跃迁
数据结构·笔记·算法·leetcode·排序算法
程序员-King.10 分钟前
day104—对向双指针—接雨水(LeetCode-42)
算法·贪心算法
笨鸟要努力19 分钟前
Qt C++ windows 设置系统时间
c++·windows·qt
笙年25 分钟前
JavaScript Promise,包括构造函数、对象方法和类方法
开发语言·javascript·ecmascript
WZTTMoon29 分钟前
Spring Boot 启动全解析:4 大关键动作 + 底层逻辑
java·spring boot·后端
章鱼哥73030 分钟前
[特殊字符] SpringBoot 自定义系统健康检测:数据库、Redis、表统计、更新时长、系统性能全链路监控
java·数据库·redis
神仙别闹33 分钟前
基于C++实现(控制台)应用递推法完成经典型算法的应用
开发语言·c++·算法
Ayanami_Reii35 分钟前
进阶数据结构应用-一个简单的整数问题2(线段树解法)
数据结构·算法·线段树·延迟标记
深圳佛手37 分钟前
Sharding-JDBC 和 Sharding-Proxy 区别
java
kk哥88991 小时前
inout参数传递机制的底层原理是什么?
java·开发语言