【洛谷】图论 图论最短路算法全解:从单源 Dijkstra 到多源 Floyd 模板与实战

文章目录


最短路问题介绍

在图 G 中,假设 vi​ 和 vj​ 为图中的两个顶点,那么 vi​ 到 vj​ 路径上所经过边的权值之和就称为带权路径长度。

由于 vi​ 到 vj​ 的路径可能有多条,将带权路径长度最短的那条路径称为最短路径。

最短路一般分为两类:

单源最短路,即图中一个顶点到其它各顶点的最短路径(一个起点)。

多源最短路,即图中每对顶点间的最短路径(多个起点)。

单源最短路

常规版 dijkstra 算法

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

常规版 dijkstra 算法流程:

小编先解释一下接下来要用到的松弛操作:

首先起点a到起点a的最短路已经确定:为0,假设a到x的最短路先前也已经通过dijkstra算法确定了:为dist[x],此时y还没确定最短路,dist[y]存储的只是从a到y任意一条路径的长度,松弛操作就是计算从起点开始,经过已经确定为最短路的结点x,到结点y的路径长度,也就是dist[x] + w,如果这个值小于dist[y],就把dist[y]修改为dist[x] + w。

松弛操作的本质:尝试用已确定最短路径的节点 x,去更新其邻接节点 y 的最短路径估计值。

• 准备⼯作:

◦ 创建⼀个⻓度为 n 的 dist 数组,其中 dist[i] 表⽰从起点到 i 结点的最短路;

◦ 创建⼀个⻓度为 n 的 bool 数组 st ,其中 st[i] 表⽰ i 点是否已经确定了最短路。

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

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

• 重复上述操作,直到所有点的最短路都确定(一共循环n - 1次,因为当n - 1个结点的最短路都确定后,这n - 1个结点的出边也就随之确定了(因为松弛操作要更新出边),这时剩下的一个结点的最短路也就间接确定了)。

代码实现

【模板】单源最短路径(弱化版)

题目描述

题目解析

小编补充一点,题目规定若不能到达则输出2^31 - 1,如果我们不知道这个数具体是多少可以借助pow函数,需要包cmath头文件。

cpp 复制代码
cout << (int)pow(2, 31) - 1 << endl;
//2147483647

实现思路基本和prim算法一致,小编就不赘述了。

注意:

1、因为本题的INF是2^31 - 1,所以不能用memset初始化dist数组,只能用for循环初始化。

2、for循环初始化dist数组时要从0开始初始化,因为标记最短路长度最短的点的变量t初始值为0,如果dist不是一个很大的数,而是0,那么t的值就永远不会改变,因为永远不会满足:dist[j] < dist[t]。
代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <cstring>
using namespace std;

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

int n, m, s;
int u, v, w;

vector<pair<int, int>> edges[M];
int dist[N];
bool st[N];

void dijkstra()
{
	//初始化
	for (int i = 0; i <= n; i++)
	{
		dist[i] = INF;
	}
	dist[s] = 0;
	//dijkstra算法
	for (int i = 1; i < n; i++)
	{
		//依次确定n个结点的最短路,循环n - 1次即可
		// 1、遍历dist,找出没有选中的点中,当前最短路长度最短的点
		int t = 0; //标记最短路长度最短的点
		for (int j = 1; j <= n; j++)
		{
			if (!st[j] && dist[j] < dist[t])
			{
				t = j;
			}
		}
		// 2、选中最短路长度最短的点
		st[t] = true;
		// 3、松弛操作
		for (auto& e : edges[t])
		{
			int a = e.first; //与t相连的结点
			int b = e.second;//与t相连的结点对应的权值
			if (dist[t] + b < dist[a])
			{
				dist[a] = dist[t] + b;
			}
		}
	}
	//输出结果
	for (int i = 1; i <= n; i++)
	{
		cout << dist[i] << " ";
	}
}

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

	return 0;
}

堆优化版 dijkstra 算法

在常规版的基础上,⽤优先级队列去维护待确定最短路的结点,因为常规版本找未选择结点中的最短路径结点的时间复杂度是O(n^2),需要遍历全部结点一遍。优化思路就是将所有结点和它的路径信息打包在一起放入堆中,以路径长度未基准排小根堆,这时松弛操作中如果满足条件:dist[a] + d < dist[c],就把改边放入堆中,每次取堆顶元素就可以拿到最短路径结点,这样就把找未选择结点中的最短路径结点的时间复杂度优化为logn(也就是维护堆结构的时间复杂度)。

代码实现

【模板】单源最短路径(标准版)

题目描述

题目解析

注意:

1、本题说明了"数据保证你能从 s 出发到任意点",所以我们把dist初始化为0x3f3f3f3f即可。

2、本题堆中可能有结点的重复路径信息,如<5, 3> <7, 3>:到结点3有两条路距离分别是5和7,但是只要我们取堆顶后把对应结点的状态置为true,后续再取堆顶元素若它已经是true直接continue就行了,不用删除堆中的重复元素。

代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <queue>
#include <cstring>
using namespace std;

typedef pair<int, int> PII;

const int N = 1e5 + 10, M = 2e5 + 10, INF = 0x3f3f3f3f;

int n, m, s;
int u, v, w;

vector<PII> edges[M]; //<结点, 路程>
//小根堆
priority_queue<PII, vector<PII>, greater<PII>> heap; //<路程,结点>
int dist[N];
bool st[N];

void dijkstra()
{
	//初始化
	memset(dist, INF, sizeof(dist));
	dist[s] = 0;
	heap.push({ 0, s });

	//dijkstra算法
	while (heap.size())
	{
		//循环拿出堆顶元素+松弛操作,直到堆为空
		//1、拿出堆顶元素+选中堆顶
		PII t = heap.top();
		heap.pop();
		int a = t.second; //结点
		if (st[a])
		{
			//已经被选过的直接continue
			continue;
		}
		st[a] = true;
		//2、松弛操作
		for (auto& e : edges[a])
		{
			int c = e.first; //结点
			int d = e.second;//路径长度
			if (dist[a] + d < dist[c])
			{
				dist[c] = dist[a] + d;
				heap.push({ dist[c], c });
			}
		}
	}
	//输出结果
	for (int i = 1; i <= n; i++)
	{
		cout << dist[i] << " ";
	}
}

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

	return 0;
}

多源最短路

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

Floyd 算法

Floyd 算法核心:

本质是动态规划,又称插点法,通过不断在两点之间加入新的点(可以理解为桥梁)来更新最短路径。

适用于任何图(有向 / 无向、边权正负),但要求最短路径存在(即无负环)。

1、状态表示

f[k][i][j] 表示:选择[1, k] 这些点中一部分(有可能选全部点也有可能一个点都不选)作为中转点,结点 i 走到结点 j 的最短路径的长度。

2、状态转移方程

不选新来的点:f[k][i][j] = f[k - 1][i][j]

选择新来的点:f[k][i][j] = f[k - 1][i][k] + f[k - 1][k][j](从i到k的最短路径加上从k到j的最短路径长度)

最终状态转移方程:f[k][i][j] = min(f[k - 1][i][j],f[k - 1][i][k] + f[k - 1][k][j])

空间优化

只会用到上一层的状态,因此可以优化掉第一维(丢掉k)。

3、初始化

f[i][i] = 0 (自己到自己距离为0)

f[i][j] 为初始状态下 i 到 j 的距离,如果没有边则为无穷。

所以f数组本质就是一个邻接矩阵。

4、填表顺序:

一定要先枚举 k,再枚举 i 和 j。因为我们填表的时候,需要依赖的是 k - 1 层的状态,因此 k 必须先枚举。

下面是整个打表流程图,可以得出一个结论,Floyd 算法是分阶段,逐步更新出最终结果。

代码实现

【模板】Floyd
题目描述

代码

注意:

1、for循环都从1开始,因为题目规定无向连通图,如果n为0表示没有结点,而图中至少有一个结点。

2、本题是无向图并且存在重边,处理输入边信息时要注意。

cpp 复制代码
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 110, INF = 0x3f3f3f3f;

int n, m;
int u, v, w;
int dp[N][N]; //邻接矩阵

int main()
{
	cin >> n >> m;
	//初始化
	memset(dp, INF, sizeof(dp));
	for (int i = 1; i <= n; i++)
	{
		//自己到自己距离为0
		dp[i][i] = 0;
	}
	//处理输入
	for (int i = 1; i <= m; i++)
	{
		cin >> u >> v >> w;
		//存无向边+处理重边
		// (min第一个参数用dp[u][v],dp[v][u]均可)
		dp[u][v] = dp[v][u] = min(dp[u][v], w);
	}
	//floyd
	//依次加入n个中转点
	for (int k = 1; k <= n; k++)
	{
		//枚举行坐标
		for (int i = 1; i <= n; i++)
		{
			//枚举列坐标
			for (int j = 1; j <= n; j++)
			{
				dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j]);
			}
		}
	}
	//输出结果
	for (int i = 1; i <= n; i++)
	{
		for (int j = 1; j <= n; j++)
		{
			cout << dp[i][j] << " ";
		}
		cout << endl;
	}
	return 0;
}

总结:

以上就是小编分享的全部内容了,如果觉得不错还请留下免费的关注和收藏
如果有建议欢迎通过评论区或私信留言,感谢您的大力支持。
一键三连好运连连哦~~

相关推荐
AI科技星2 小时前
基于v=c空间本底光速螺旋运动的宏观力方向第一性原理推导:太阳系与地球系统的全维度观测验证
人工智能·线性代数·算法·机器学习·平面
Epiphany.5562 小时前
炸雷!(地址映射+dfs搜索)
算法
Crazyong2 小时前
FreeRTOS-互斥量-2
算法
啊我不会诶2 小时前
2025 北京市大学生程序设计竞赛暨“小米杯”全国邀请赛
c++·学习·算法
mit6.8242 小时前
懒更新|单点查询
算法
Yupureki2 小时前
《C++实战项目-高并发内存池》8. 最终性能优化与测试
c语言·开发语言·数据结构·c++·算法·性能优化
DeepModel2 小时前
【概率分布】均匀分布的原理、推导与Python实现
python·算法·概率论
一叶落4382 小时前
LeetCode 74 | 搜索二维矩阵(C语言版题解)
c语言·数据结构·c++·算法·leetcode·矩阵·动态规划
罗湖老棍子2 小时前
星际信号塔 —— 单调栈经典应用详解
数据结构·算法·单调栈