C++图论基础单源最短路-常规版dijkstra算法/堆优化版dijkstra算法/bellman-ford 算法/spfa 算法流食般投喂

本编标红字段均是值得纳为己用的经验条~


先来介绍一下什么是最短路径:

在图G中,假设Vi和Vj是两个顶点,从Vi到Vj所经过的边权之和称为带权路径长度。

由于Vi到Vj的路径可能有多条,带权路径长度最短的就称为了最短路径。

最短路一般分为两类:

单源最短路:即图中一个顶点到其他各顶点的最短路径。

多源最短路:即图中每对顶点的最短路径。


常规版dijkstra算法(迪杰斯特拉):

首先是要有两个数组:

int distN :整体弄完,里面存的就是从某一点出发到所有点的最短路径。bool stN

disti表示从起点到 i 的最短路径;sti表示 i 点是否已经确定最短路径。

这里给个三板斧:

1、初始化:dist数组从0到n全部先初始化为无穷或是指定值,在把起点的dist初始化为0,st数组都是false。

2、单次操作:从dist数组中选出未确定最短路且此时最短路最小的点u;找出点后标记成确定true;根据新确定最短路的点u,进行松弛操作(就是从新确定的点u开始,看看从这个点开始有没有可能会出现一个比现存值更小的值,出现就更新一下)。

3、重复 n - 1 次步骤二。

***dijkstra算法不能用于边权有负值的情况,本质的贪心策略就不正确了。


OJ题来源:洛谷

OJ题名:【模板】单源最短路径(弱化版)

OJ题归属:图论基础【单源最短路】

解题算法:vector存图 + 常规版dijkstra算法(迪杰斯特拉)

经验总结:本题有个2的31次方减1的数,我们如果不知道的话,就用编译器cout出来。

这一题之所以是弱化版的模板,就是因为数据量不够大,只有1e4级别,而常规版dijkstra算法是O(n^2)的,不会超时,但是在标准版的模板题那,就会超时了。

cpp 复制代码
#include<iostream>
#include<vector>
#include<cmath>

using namespace std;

typedef pair<int, int> PII;

const int N = 1e4 + 10, INF = 2147483647;

int n, m, s;
vector<PII> edges[N];
int dist[N];
bool st[N];

int main()
{
	cin >> n >> m >> s;
	for (int i = 1; i <= m; i++)
	{
		int u, v, w; cin >> u >> v >> w;
		edges[u].push_back({ v, w });
	}

	// 初始化
	for (int i = 0; i <= n; i++) dist[i] = INF;
	dist[s] = 0;

	// 开始 n - 1 次重复操作
	for (int i = 1; i < n; i++)
	{
		// 1.找未确定最短路径最小的点
		int a = 0;
		for (int j = 1; j <= n; j++)
		{
			if (!st[j] && dist[j] < dist[a]) a = j;
		}

		// 2.标记成确定,进行松弛操作
		st[a] = true;

		for (auto& e : edges[a])
		{
			int b = e.first, c = e.second;

			if (dist[b] > dist[a] + c) dist[b] = dist[a] + c;
		}
	}

	for (int i = 1; i <= n; i++) cout << dist[i] << " ";

	return 0;
}

堆优化版的dijkstra算法:

这个是在常规版的基础上进行了时间复杂度的优化,在常规版的dijkstra算法中,每一次找 "未确定最短路径且此时最短路径最小的点",都会遍历一遍dist数组,时间复杂度很高。

优化版本就在找点的这里用小根堆进行了优化,每一次找点就从堆顶就可以拿到值,总体时间复杂度可以降到O(M*logM)。

当然,用堆优化,也会带来新的难点,首先,堆存储的数据形式是这样的<距离,点>,其次,在进行松弛操作的时候,同一点、不同距离的数据都会存储于堆中,我们在找点时,找到点后,看看此点有没有确定最短路径,如果确定了,那么其他相同节点的数据都是无效数据,他们的最短路径肯定是比已选的大,不符合要求了,直接特判continue掉。


OJ题来源:洛谷

OJ题名:【模板】单源最短路径(标准版)

OJ题归属:图论基础【单源最短路】

解题算法:vector存图 + 堆优化版dijkstra算法(迪杰斯特拉)

经验总结:堆优化版的dijkstra算法,不再强制执行 n - 1 次重复操作,而是遍历完堆中所有数据,因为用堆优化带来的问题就决定了堆这个数据结构里面会存下所有可能的路径数据,只有把存在堆里面的数据全部遍历一边,才能确定最终dist数组中各个格子的值!

cpp 复制代码
#include<iostream>
#include<vector>
#include<queue>
#include<cstring>

using namespace std;

typedef pair<int, int> PII;

const int N = 1e5 + 10;

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, 0x3f3f3f3f, sizeof dist);
	dist[s] = 0;
	heap.push({ 0, s });

	// 堆里面最后会存下所有可能的路径,所以就不需要像常规版那样强制执行 n - 1 次,
	// 这里是要把存在堆里面的所有数据都过一遍
	while (heap.size())
	{
		// 1.找未确定最短路径且此时最短路径最小的点
		auto x = heap.top(); heap.pop();
		int a = x.second;

		// 会有同一节点,不同最短路径的数据放入堆中,特判一下,
		// 这里面的数据只会拿掉最短路径的那个数据,其他数据都是无效的
		if (st[a]) continue;

		// 标记成确定最短路径的点
		st[a] = true;

		// 松弛操作,比一比,看看有没有更合适的数据出现
		for (auto& e : edges[a])
		{
			int b = e.first, c = e.second;

			if (dist[a] + c < dist[b])
			{
				dist[b] = dist[a] + c;
				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, w; cin >> u >> v >> w;
		edges[u].push_back({ v, w });
	}

	dijkstra();

	return 0;
}

Bellman-Ford 算法

该算法适用于边权有负数的情况,而且还可以判断负环,存在负环的是没有最短路径的,下篇详讲。

该算法的本质是对所有边进行至多 n - 1 次的松弛操作,很暴力,但也很有用,具体操作流程如下:

1、首先是要有一个dist数组,和迪杰斯特拉算法有着一样的含义,即"disti表示从起点到 i 位置的最短路径";但是此处并没有st数组哦。

2、单趟操作:每次对所有边都进行松弛操作~

2.1、先枚举点,再枚举出边,这样就枚举了所有边。

2.2、当当前点还是无穷大的时候,可以直接特判continue掉,因为进行了松弛操作也没啥意义,而且数据会溢出。

3、重复 n - 1 次单趟操作~

3.1、有的情况不需要跑满 n - 1 次,可以用bool flag 进行对每次单趟的判断,当有一次单趟没有进行松弛操作时,就可以break跳出 n - 1 次循环了。


OJ题来源:洛谷

OJ题名:【模板】单源最短路径(弱化版)

OJ题归属:图论基础【单源最短路】

解题算法:vector存图 + Bellman-Ford 算法

cpp 复制代码
#include<iostream>
#include<vector>
#include<cmath>

using namespace std;

typedef pair<int, int> PII;

const int N = 1e4 + 10, INF = 2147483647;

int n, m, s;
vector<PII> edges[N];
int dist[N];

void bellman_ford()
{
	// 初始化
	for (int i = 0; i <= n; i++) dist[i] = INF;
	dist[s] = 0;

	// 优化一下:有些情况可能不需要跑满 n - 1 次
	bool flag = false;
	// 重复 n - 1 次松弛操作
	for (int i = 1; i < n; i++)
	{
		// 单趟检测
		flag = false;

		// 枚举所有边
		for (int u = 1; u <= n; u++) // 枚举各个点
		{
			if (dist[u] == INF) continue;

			for (auto& e : edges[u]) // 枚举各个点的出边
			{
				int v = e.first, w = e.second;

				if (dist[v] > dist[u] + w)
				{
					dist[v] = dist[u] + w;
					flag = true;
				}
			}
		}

		if (flag == false) break;
	}

	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, w; cin >> u >> v >> w;
		edges[u].push_back({ v, w });
	}

	bellman_ford();

	return 0;
}

spfa 算法:

spfa算法其实就是用队列优化的Bellman-Ford算法,因为我们知道,Bellman-Ford算法中,在执行单次操作时,并不是所有边都进行了有效的松弛操作,只有经松弛操作之后的节点的出边才可能会继续有松弛操作,所以,我们用了队列来优化,队列里面存着经过松弛操作后待处理节点,从队列里面拿出来时,这个节点在呆在队列的过程中,别的节点进行松弛操作,连带着他已经经过一次或者多次,已经充分进行了松弛操作,所以,出队列时,我们拿到的就是最新最好的结果,这样我们也不要有"同一节点,多个同时在队列里面"的行为。

spfa算法这里新增了一个 bool stN 数组,sti表示 i 节点是否在队列里面,和之前的迪杰斯特拉算法是不一样的含义哦。

***这个算法最差情况是O(nm)的,出题人可以出这种图卡这个时间复杂度,标准版那个题就是。


OJ题来源:洛谷

OJ题名:【模板】单源最短路径(弱化版)

OJ题归属:图论基础【单源最短路】

解题算法:vector存图 + spfa 算法

cpp 复制代码
#include<iostream>
#include<vector>
#include<queue>
#include<cmath>

using namespace std;

typedef pair<int, int> PII;

const int N = 1e4 + 10, INF = 2147483647;

int n, m, s;
vector<PII> edges[N];
int dist[N];
bool st[N];

void spfa()
{
	// 初始化
	for (int i = 0; i <= n; i++) dist[i] = INF;

	queue<int> q;
	q.push(s);
	dist[s] = 0;
	st[s] = true;

	while (q.size())
	{
		auto a = q.front(); q.pop();
		st[a] = false;

		for (auto& e : edges[a])
		{
			int b = e.first, c = e.second;

			if (dist[a] + c < dist[b])
			{
				dist[b] = dist[a] + c;
				if (!st[b])
				{
					q.push(b);
					st[b] = true;
				}
			}
		}
	}

	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, w; cin >> u >> v >> w;

		edges[u].push_back({ v, w });
	}

	spfa();

	return 0;
}
相关推荐
摇滚侠1 小时前
MyBatis 入门到项目实战 MyBatis 逆向工程 62
java·开发语言·mybatis
颖火虫盟主1 小时前
Linux USB 探测→枚举→RNDIS 驱动匹配 全流程笔记
linux·运维·笔记
ch.ju1 小时前
Java Programming Chapter 4——Multi-level inheritance
java·开发语言
长葡萄的叶子1 小时前
Transformer:让机器读懂上下文的艺术
笔记·transformer
相醉为友1 小时前
Trae IDE WSL2/SSH 环境网络故障排查笔记
ide·笔记·ssh
Molesidy1 小时前
【Linux】【C++】Linux下的C++编程以及基于GDB的VSCode的C++调试
开发语言·c++
techdashen1 小时前
用 Rust 真正发出 Ping:FFI 类型、newtype 与 MaybeUninit
开发语言·后端·rust
塵觴葉1 小时前
基于Lua协程的简单任务管理
开发语言·lua
浮芷.1 小时前
鸿蒙 6.1 新特性-60fps流畅人物跳跃功能算法深度解析-鸿蒙PC端正弦值计算法
算法·华为·harmonyos·鸿蒙·鸿蒙系统