文章目录
最短路问题介绍
在图 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;
}
总结:

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