最短路径算法刷题笔记

Dijkstra最短路算法

  1. 初始化

    • 使用 memsetvis 数组的所有元素初始化为 false,表示所有顶点都未被访问。
    • 使用 memsetdist 数组的所有元素初始化为 INF,表示所有顶点到源点的初始距离为无穷大。
    • 将源点 sdist 数组中对应的距离设置为 0
  2. 外层循环

    • 进行 n-1 次循环,n 是顶点的总数。每次循环选择一个顶点加入到已最短路径确定的集合中。
  3. 寻找未访问的最近顶点

    • 在外层循环中,首先寻找一个未访问的且距离源点最近的顶点 u。这是通过内层循环实现的,内层循环遍历所有顶点,找到满足条件的顶点 u
  4. 标记已访问

    • 将找到的最近顶点 uvis 数组中对应的位置标记为 true,表示该顶点已经被访问。
  5. 松弛操作

    • 对于顶点 u 的所有邻接顶点 v,进行松弛操作(relaxation)。如果通过顶点 u 可以找到到顶点 v 的更短路径,则更新 dist[v] 的值。具体操作是检查 dist[v] 是否大于 dist[u] + g[u][v],如果是,则更新 dist[v]
  6. 重复

    • 重复步骤 3-5,直到所有顶点都被访问,或者达到其他停止条件。

理论

代码来自chatgpt,我感觉代码很好,比我在网上找到的好理解很多

#include <iostream>
#include <cstring>
using namespace std;
const int N = 110;
const int INF = 0x3f3f3f3f;
int n, m;
int g[N][N]; // 邻接矩阵表示图
int dist[N]; // 记录源点到每个点的最短距离
bool vis[N]; // 记录每个点是否已访问
void dijkstra(int s) {
    memset(vis, false, sizeof(vis)); // 初始化访问数组,所有顶点都未访问
    memset(dist, 0xFF, sizeof(dist)); // 初始化距离数组,所有点到源点的距离设为无穷大
    dist[s] = 0; // 源点到自己的距离为0
    for (int i = 0; i < n; ++i) { // 进行 n-1 次循环
        int u = -1;
        // 寻找未访问的最近顶点
        for (int j = 1; j <= n; ++j) {
            if (!vis[j] && (u == -1 || dist[j] < dist[u])) {
                u = j; // u 是未访问的最近顶点
            }
        }
        // 标记顶点 u 为已访问
        vis[u] = true;
        // 更新顶点 u 到所有其他未访问顶点的距离
        for (int v = 1; v <= n; ++v) {
            if (!vis[v] && g[u][v] && dist[v] > dist[u] + g[u][v]) {
                dist[v] = dist[u] + g[u][v]; // 松弛操作
            }
        }
    }
}

int main()
{
    cin >> n >> m;
    memset(g, INF, sizeof(g));
    for (int i = 1; i <= n; i++) g[i][i] = 0;
    for (int i = 0; i < m; i++)
    {
        int a, b, c;
        cin >> a >> b >> c;
        g[a][b] = g[b][a] = min(g[a][b], c); // 无向图
    }
    dijkstra(1);
    for (int i = 1; i <= n; i++) cout << dist[i] << " ";
    cout << endl;
    return 0;
}

带负权则无法处理,不能使用Dijkstra算法

Dijkstra算法以 出发。D——点从剩下的点里的最短路dis最小的出发

SPFA单源最短路算法

算是bellman-ford算法

在这里插入图片描述
对于稀疏图来说,比Dijkstra算法快
SPFA算法可以用于有负权图有负环则不行
一般能用Dijstra算法则用Dijstra算法

在这里插入图片描述

SPFA算法以出发bellman-ford,b——边 SPFA用到队列,将源点相关联的点放到队列中然后再从队列中的点取寻找
SPFA算法是一种求解单源最短路径的算法,可以处理带有负权边的图。除了使用邻接表来存储图的信息之外,我们还可以使用邻接矩阵来存储图的信息。下面是不使用邻接表,使用邻接矩阵的方式实现SPFA算法的代码,并附有详细注释。

#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 5; // 最大顶点数
const int inf = 0x3f3f3f3f; // 无穷大
int n, m; // 顶点数、边数
int graph[maxn][maxn]; // 邻接矩阵表示
int d[maxn], vis[maxn]; // 源点到各个顶点的最短路径长度、是否在队列中
queue<int> q; // 队列
// SPFA算法
void spfa(int s) {
    memset(d, inf, sizeof(d)); // 初始化源点到其他顶点的最短路径长度为无穷大
    memset(vis, 0, sizeof(vis)); // 初始化所有顶点为不在队列中
    d[s] = 0; // 源点到自己的最短路径长度为0
    vis[s] = 1; // 将源点加入队列中
    q.push(s);
    while (!q.empty()) {
        int x = q.front();
        q.pop();
        vis[x] = 0; // 标记为不在队列中
        for (int y = 1; y <= n; y++) { // 遍历x的所有相邻顶点
            if (graph[x][y] != inf && d[y] > d[x] + graph[x][y]) { // 松弛操作
                d[y] = d[x] + graph[x][y];
                if (!vis[y]) { // 如果y不在队列中,就将y加入队列中
                    vis[y] = 1;
                    q.push(y);
                }
            }
        }
    }
}
int main() {
    cin >> n >> m;
    memset(graph, inf, sizeof(graph)); // 初始化邻接矩阵
    for (int i = 1; i <= m; i++) {
        int a, b, c;
        cin >> a >> b >> c;
        graph[a][b] = c;
    }
    spfa(1); // 以1为源点进行最短路径计算
    for (int i = 1; i <= n; i++) {
        cout << d[i] << " ";
    }
    cout << endl;
    return 0;
}

在上述代码中,我们使用一个二维数组graph来存储图的信息,其中graph[i][j]表示从顶点i到顶点j的边权重值。初始化时,我们将所有的graph[i][j]都赋值为无穷大,表示该边不存在。在遍历顶点x的所有相邻顶点y时,我们需要判断graph[x][y]是否等于无穷大,如果等于无穷大,那么表示顶点x和y之间不存在边。如果d[y] > d[x] + graph[x][y],那么就进行松弛操作,并将顶点y加入队列中。

判断负环

需要注意的是,当图中存在负权环时,SPFA算法可能会陷入死循环。此时,我们可以设置一个变量count,表示顶点x进入队列的次数,当count达到n时,就说明顶点x进入队列的次数已经超过了n次,即图中存在负权环。我们可以在代码中加入如下代码来判断:

int count[maxn]; // 记录顶点进入队列的次数
// SPFA算法
void spfa(int s) {
    // 省略部分代码
    while (!q.empty()) {
        int x = q.front();
        q.pop();
        vis[x] = 0; // 标记为不在队列中
        for (int y = 1; y <= n; y++) { // 遍历x的所有相邻顶点
            if (graph[x][y] != inf && d[y] > d[x] + graph[x][y]) { // 松弛操作
                d[y] = d[x] + graph[x][y];
                if (!vis[y]) { // 如果y不在队列中,就将y加入队列中
                    vis[y] = 1;
                    q.push(y);
                    count[y]++;
                    if (count[y] > n) { // 如果顶点y进入队列的次数已经超过了n次,那么就说明存在负权环
                        cout << "图中存在负权环" << endl;
                        return;
                    }
                }
            }
        }
    }
}

Floyd多源最短路算法

  1. 初始化:创建一个大小为 n × n n \times n n×n 的矩阵 dist,其中 n 是顶点的数量。初始化矩阵,使得 dist[i][j] 表示顶点 i 到顶点 j 的直接路径的权重(如果有的话),如果顶点 i 和顶点 j 之间没有直接相连,则 dist[i][j] 可以初始化为无穷大(INF)。
  2. 填充对角线:将矩阵的对角线元素(dist[i][i])设置为0,因为从任何一个顶点到它自己的路径长度是0。
  3. 动态规划:使用三层嵌套循环来更新 dist 矩阵中的每个元素。外层循环遍历顶点 k,表示当前考虑的中间顶点。中间层循环遍历所有顶点 i 作为起始顶点。内层循环遍历所有顶点 j 作为终点。对于每个 i 和 j,检查是否通过顶点 k 可以找到更短的路径。
  4. 更新最短路径:如果在 dist[i][k] + dist[k][j] 的路径比当前 dist[i][j] 更短,则更新 dist[i][j]。
const int MAXN = 100;  // 定义最大节点数
int dist[MAXN][MAXN];   // 定义距离矩阵
void floyd(int n) {     // n为节点数
    // 初始化距离矩阵
    for(int i = 1; i <= n; i++) {
        for(int j = 1; j <= n; j++) {
            if(i == j) {
                dist[i][j] = 0;   // 自己到自己的距离为0
            } else {
                dist[i][j] = INF; // 初始化为无穷大
            }
        }
    }
    // 输入边权值
    int m;  // 边数
    cin >> m;
    for(int i = 1; i <= m; i++) {
        int u, v, w;
        cin >> u >> v >> w;
        dist[u][v] = w;   // 有边的节点之间权值为w
    }
    // Floyd算法核心
    for(int k = 1; k <= n; k++) {   // 枚举中间节点
        for(int i = 1; i <= n; i++) {   // 枚举起点
            for(int j = 1; j <= n; j++) {   // 枚举终点
                dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);   // 更新最短距离
            }
        }
    }
}
int main() {
    int n;  // 节点数
    cin >> n;
    floyd(n);  // 调用Floyd算法
    return 0;
}

次短路问题

在这里插入图片描述
在这里插入图片描述

练习

[蓝桥杯 2022 国 B] 出差

A \mathrm{A} A 国有 N N N 个城市,编号为 1 … N 1 \ldots N 1N 小明是编号为 1 1 1 的城市中一家公司的员工,今天突然接到了上级通知需要去编号为 N N N 的城市出差。

由于疫情原因,很多直达的交通方式暂时关闭,小明无法乘坐飞机直接从城市 1 1 1 到达城市 N N N,需要通过其他城市进行陆路交通中转。小明通过交通信息网,查询到了 M M M 条城市之间仍然还开通的路线信息以及每一条路线需要花费的时间。

同样由于疫情原因,小明到达一个城市后需要隔离观察一段时间才能离开该城市前往其他城市。通过网络,小明也查询到了各个城市的隔离信息。(由于小明之前在城市 1 1 1,因此可以直接离开城市 1 1 1,不需要隔离)

由于上级要求,小明希望能够尽快赶到城市 N \mathrm{N} N, 因此他求助于你,希望你能帮他规划一条路线,能够在最短时间内到达城市 N N N

输入格式

1 1 1 行:两个正整数 N , M N, M N,M 表示 A 国的城市数量, M M M 表示末关闭的路线数量。

2 2 2 行: N N N 个正整数,第 i i i 个整数 C i C_{i} Ci 表示到达编号为 i \mathrm{i} i 的城市后需要隔离的时间。

3 … M + 2 3 \ldots M+2 3M+2 行: 每行 3 3 3 个正整数, u , v , c u, v, c u,v,c, 表示有一条城市 u u u 到城市 v v v 的双向路线仍然开通着,通过该路线的时间为 c c c

输出格式

1 1 1 行: 1 1 1 个正整数,表示小明从城市 1 1 1 出发到达城市 N N N 的最短时间。(到达城市 N N N,不需要计算城市 N N N 的隔离时间)

样例输入 #1

4 4
5 7 3 4
1 2 4
1 3 5
2 4 3
3 4 5

样例输出 #1

13

提示

【样例说明】

【评测用例规模与约定】

对于 100 % 100 \% 100% 的数据, 1 ≤ N ≤ 1000 , 1 ≤ M ≤ 10000 , 1 ≤ C i ≤ 200 , 1 ≤ u , v ≤ 1 \leq N \leq 1000,1 \leq M \leq 10000,1 \leq C_{i} \leq 200,1 \leq u, v \leq 1N1000,1M10000,1Ci200,1u,v N , 1 ≤ c ≤ 1000 N, 1 \leq c \leq 1000 N,1c1000

蓝桥杯 2022 国赛 B 组 E 题。

思路

  • 从1出发到 N N N,可以想到 d i j k s t r a dijkstra dijkstra算法(没有负权)。我想的就是经典 d i j k s t r a dijkstra dijkstra算法,题目中说每个城市有隔离时间,所以再 d i s dis dis处加上后者城市的隔离时间。最后输出 d i s [ N ] dis[N] dis[N]即可
  • 别人的思路:基本与我一样

题解

#include<bits/stdc++.h>
using namespace std;
const int N=1010;
int gra[N][N];
int dist[N];
int g[N];
bool st[N];
int n,m;
int dijkstra()
{
	 memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    for (int i = 0; i < n - 1; i ++ )
    {
        int t = -1;
        for (int j = 1; j <= n; j ++ )
            if (!st[j] && (t == -1 || dist[t] > dist[j]))
                t = j;
        for (int j = 1; j <= n; j ++ )
            dist[j] = min(dist[j], dist[t] + gra[t][j]);
        st[t] = true;
    }
	return dist[n];
}
int main() 
{
	cin>>n>>m;
	for(int i=1;i<=n;++i) cin>>g[i];
	g[n]=0;
	memset(gra, 0x3f, sizeof gra);
	for(int i=1;i<=m;++i){
		int u,v,c;
		cin>>u>>v>>c;
		gra[u][v]=g[v]+c;
		gra[v][u]=g[u]+c;
	}
	cout<<dijkstra()<<endl;
}

邮递员送信

有一个邮递员要送东西,邮局在节点 1 1 1。他总共要送 n − 1 n-1 n1 样东西,其目的地分别是节点 2 2 2 到节点 n n n。由于这个城市的交通比较繁忙,因此所有的道路都是单行的,共有 m m m 条道路。这个邮递员每次只能带一样东西,并且运送每件物品过后必须返回邮局。求送完这 n − 1 n-1 n1 样东西并且最终回到邮局最少需要的时间。

输入格式

第一行包括两个整数, n n n m m m,表示城市的节点数量和道路数量。

第二行到第 ( m + 1 ) (m+1) (m+1) 行,每行三个整数, u , v , w u,v,w u,v,w,表示从 u u u v v v 有一条通过时间为 w w w 的道路。

输出格式

输出仅一行,包含一个整数,为最少需要的时间。

样例输入 #1

5 10
2 3 5
1 5 5
3 5 6
1 2 8
1 3 8
5 3 4
4 1 8
4 5 3
3 5 6
5 4 2

样例输出 #1

83

提示

对于 30 % 30\% 30% 的数据, 1 ≤ n ≤ 200 1 \leq n \leq 200 1n200

对于 100 % 100\% 100% 的数据, 1 ≤ n ≤ 1 0 3 1 \leq n \leq 10^3 1n103 1 ≤ m ≤ 1 0 5 1 \leq m \leq 10^5 1m105 1 ≤ u , v ≤ n 1\leq u,v \leq n 1u,vn 1 ≤ w ≤ 1 0 4 1 \leq w \leq 10^4 1w104,输入保证任意两点都能互相到达。

思路

  • 看完题目,可以得知:要实现邮递员从 1 到 n-1 是比较简单的,基本就是套模板
  • 根据题目不难得知是有向边。从 n-1 到 1 ,如果用spfa算法遍历 n-1 到 1 的话,时间复杂度是O(n3),大概率TLE。所以不先考虑这个方法
  • 考虑建立反图:在邮递员回去的这段中,将图上所有的边取反向,那么从 1 到 n-1 的最短路即是 非反图中 n-1 到 1 的最短路

题解

#include<bits/stdc++.h>
using namespace std;
const int INF=0x3f3f3f3f;
int n,m,u[100005],v[100005],w[100005],dis[1005],ans=0;
void ford(){
	for(int i=1;i<=n;i++)dis[i]=INF;
	dis[1]=0;
	for(int k=1;k<=n-1;k++){
		for(int i=1;i<=m;i++){
			if(dis[v[i]]>dis[u[i]]+w[i]){
				dis[v[i]]=dis[u[i]]+w[i];
			}
		}
	}
	for(int i=1;i<=n;i++)ans+=dis[i];
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++)scanf("%d%d%d",&u[i],&v[i],&w[i]);
	ford();
	for(int i=1;i<=m;i++) swap(u[i],v[i]);
	ford();
	printf("%d\n",ans);
}

[蓝桥杯 2018 国 B] 调手表

小明买了块高端大气上档次的电子手表,他正准备调时间呢。

在 M78 星云,时间的计量单位和地球上不同,M78 星云的一个小时有 n n n 分钟。

大家都知道,手表只有一个按钮可以把当前的数加一。在调分钟的时候,如果当前显示的数是 0 0 0,那么按一下按钮就会变成 1 1 1,再按一次变成 2 2 2。如果当前的数是 n − 1 n-1 n1,按一次后会变成 0 0 0

作为强迫症患者,小明一定要把手表的时间调对。如果手表上的时间比当前时间多 1 1 1,则要按 n − 1 n-1 n1 次加一按钮才能调回正确时间。

小明想,如果手表可以再添加一个按钮,表示把当前的数加 k k k 该多好啊……

他想知道,如果有了这个 + k +k +k 按钮,按照最优策略按键,从任意一个分钟数调到另外任意一个分钟数最多要按多少次。

注意,按 + k +k +k 按钮时,如果加 k k k 后数字超过 n − 1 , n-1, n1, 则会对 n n n 取模。

比如, n = 10 , k = 6 n=10,k=6 n=10,k=6 的时候,假设当前时间是 0 0 0,连按 2 2 2 + k +k +k 按钮,则调为 2 2 2

输入格式

一行两个整数 n , k n,k n,k,意义如题。

输出格式

一行一个整数。表示:按照最优策略按键,从一个时间调到另一个时间最多要按多少次。

样例输入 #1

5 3

样例输出 #1

2

提示

【样例解释】

如果时间正确则按 0 0 0 次。否则要按的次数和操作系列之间的关系如下:

  1. +1
  2. +1, +1
  3. +3
  4. +3, +1

【数据约定】

对于 30 % 30\% 30% 的数据 0 < k < n ≤ 5 0<k<n \le 5 0<k<n5

对于 60 % 60\% 60% 的数据 0 < k < n ≤ 100 0<k<n \le 100 0<k<n100

对于 100 % 100\% 100% 的数据 0 < k < n ≤ 1 0 5 0<k<n \le 10^5 0<k<n105

时限 3 秒, 256M。蓝桥杯 2018 年第九届国赛

思路

  • 我的思考:一开始想到的是动态规划。状态就是方案数,根据题目给出的两个按钮,以数学规律的形式表示状态转移规律

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

2018 年 7 月 19 日,某位同学在 NOI Day 1 T1 归程 一题里非常熟练地使用了一个广为人知的算法求最短路。

然后呢?

100 → 60 100 \rightarrow 60 10060

Ag → Cu \text{Ag} \rightarrow \text{Cu} AgCu

最终,他因此没能与理想的大学达成契约。

小 F 衷心祝愿大家不再重蹈覆辙。


给定一个 n n n 个点, m m m 条有向边的带非负权图,请你计算从 s s s 出发,到每个点的距离。

数据保证你能从 s s s 出发到任意点。

输入格式

第一行为三个正整数 n , m , s n, m, s n,m,s
第二行起 m m m 行,每行三个非负整数 u i , v i , w i u_i, v_i, w_i ui,vi,wi,表示从 u i u_i ui v i v_i vi 有一条权值为 w i w_i wi 的有向边。

输出格式

输出一行 n n n 个空格分隔的非负整数,表示 s s s 到每个点的距离。

样例输入 #1

4 6 1
1 2 2
2 3 2
2 4 1
1 3 5
3 4 3
1 4 4

样例输出 #1

0 2 4 3

提示

样例解释请参考 数据随机的模板题

1 ≤ n ≤ 1 0 5 1 \leq n \leq 10^5 1n105

1 ≤ m ≤ 2 × 1 0 5 1 \leq m \leq 2\times 10^5 1m2×105

s = 1 s = 1 s=1

1 ≤ u i , v i ≤ n 1 \leq u_i, v_i\leq n 1ui,vin

0 ≤ w i ≤ 1 0 9 0 \leq w_i \leq 10 ^ 9 0wi109,

0 ≤ ∑ w i ≤ 1 0 9 0 \leq \sum w_i \leq 10 ^ 9 0wi109

思路

  • 模板题,用好Dijkstra算法+堆优化
  • 虽然Dijkstra算法思想好理解,但是实现起来还是有点复杂的
  • 具体看代码注释

题解

#include<bits/stdc++.h>
const int MaxN = 100010, MaxM = 500010;
struct edge{
    int to, dis, next;
};
edge e[MaxM];
int head[MaxN], dis[MaxN], cnt;
bool vis[MaxN];
int n, m, s;
inline void add_edge( int u, int v, int d ){
    cnt++;
    e[cnt].dis = d;
    e[cnt].to = v;
    e[cnt].next = head[u];
    head[u] = cnt; //这个head以链表的形式连接点,分析下面给出的图就会发现,通过head数组即可找到u点连接的所有点
}

struct node{
    int dis;
    int pos;
    bool operator <( const node &x )const
    {
        return x.dis < dis;
    }
};

std::priority_queue<node> q;

inline void dijkstra(){
    dis[s] = 0;
    q.push( ( node ){0, s} );
    while( !q.empty() ){
        node tmp = q.top();
        q.pop();
        int x = tmp.pos, d = tmp.dis;
        if( vis[x] )continue;
        vis[x] = 1;
        for( int i = head[x]; i; i = e[i].next ){     //循环:i=e[i].next就可以发现相关联的点都被连接起来了
            int y = e[i].to;
            if( dis[y] > dis[x] + e[i].dis ){
                dis[y] =std::min(dis[y],dis[x] + e[i].dis);
                if( !vis[y] ) q.push( ( node ){dis[y], y} );
            }
        }
    }
}


int main(){
    scanf( "%d%d%d", &n, &m, &s );
    for(int i = 1; i <= n; ++i)dis[i] = 0x7fffffff;
    for( register int i = 0; i < m; ++i ){
        register int u, v, d;
        scanf( "%d%d%d", &u, &v, &d );
        add_edge( u, v, d );
    }
    dijkstra();
    for( int i = 1; i <= n; i++ )
        printf( "%d ", dis[i] );
    return 0;
}

在这里插入图片描述
在这里插入图片描述

感觉Dijkstra算法代码和SPFA代码有些相似之处

总结

在解决最短路径问题时,应根据问题的具体情况选择合适的算法,以下是各算法适用情况的简单介绍:

  • Dijkstra算法:适用于求解单源最短路径问题,即求解一个点到其他所有点的最短路径,且边权非负。Dijkstra算法通过贪心策略,每次选择当前距离源点最近的未访问点进行松弛操作,从而逐步确定源点到其他点的最短路径。
  • SPFA算法:适用于求解单源最短路径问题,且边权可以为负数。SPFA算法基于Bellman-Ford算法,采用队列优化的方式进行松弛操作,可以在某些情况下比Dijkstra算法更快。
  • Floyd算法:适用于求解任意两点之间的最短路径,且边权可以为负数。Floyd算法采用动态规划的思想,通过中间点的遍历,逐步确定任意两点之间的最短路径,相对于Dijkstra算法和SPFA算法,Floyd算法的时间复杂度更高,但适用于稠密图。
  • 在实际应用中,应根据问题的具体情况选择合适的算法,对于稀疏图,可以使用Dijkstra算法或SPFA算法,对于稠密图或需要求解任意两点之间的最短路径问题,可以使用Floyd算法。
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值