最短路问题(详解)
前言
最短路问题分为两个模块,一个是单源最短路,一个是多源汇最短路。而其中有4个算法。所以可以分别总结一下。
Dijkstra 算法
这里介绍 Dijkstra 算法,它是一个应用最为广泛的、名气也是最大的单源最短路径算法Dijkstra 算法有一定的局限性:它所处理的图中不能有负权边
「前提:图中不能有负权边」
换句话说,如果一张图中,但凡有一条边的权值是负值,那么使用 Dijkstra 算法就可能得到错误的结果不过,在实际生活中所解决的问题,大部分的图是不存在负权边的
如:有一个路线图,那么从一点到另外一点的距离肯定是一个正数,所以,虽然 Dijkstra 算法有局限性,但是并不影响在实际问题的解决中非常普遍的来使用它
看如下实例:
(1)初始
左边是一张连通带权有向图,右边是起始顶点 0 到各个顶点的当前最短距离的列表,起始顶点 0 到自身的距离是 0
(2)将顶点 0 进行标识,并作为当前顶点
对当前顶点 0 的所有相邻顶点依次进行松弛操作,同时更新列表从列表的未标识顶点中找到当前最短距离最小的顶点,即 顶点 2,就可以说,起始顶点 0 到顶点 2 的最短路径即 0 -> 2
因为:图中没有负权边,即便存在从顶点 1 到顶点 2 的边,也不可能通过松弛操作使得从起始顶点 0 到顶点 2 的距离更小
图中没有负权边保证了:对当前顶点的所有相邻顶点依次进行松弛操作后,只要能从列表的未标识顶点中找到当前最短距离最小的顶点,就能确定起始顶点到该顶点的最短路径
(3)将顶点 2 进行标识,并作为当前顶点
(4)对当前顶点 2 的相邻顶点 1 进行松弛操作,同时更新列表
(5)对当前顶点 2 的相邻顶点 4 进行松弛操作,同时更新列表
(6)对当前顶点 2 的相邻顶点 3 进行松弛操作,同时更新列表
从列表的未标识顶点中找到当前最短距离最小的顶点,即 顶点 1,
就可以说,起始顶点 0 到顶点 1 的最短路径即 0 -> 2 -> 1
(7)将顶点 1 进行标识,并作为当前顶点
(8)对当前顶点 1 的相邻顶点 4 进行松弛操作,同时更新列表
从列表的未标识顶点中找到当前最短距离最小的顶点,即 顶点 4,就可以说,起始顶点 0 到顶点 4 的最短路径即 0 -> 2 -> 1 -> 4
(9)将顶点 4 进行标识,并作为当前顶点
当前顶点 4 没有相邻顶点,不必进行松弛操作
从列表的未标识顶点中找到当前最短距离最小的顶点,即 顶点 3,就可以说,起始顶点 0 到顶点 3 的最短路径即 0 -> 2 -> 3
(10)将顶点 3 进行标识,并作为当前顶点
对当前顶点 3 的相邻顶点 4 进行松弛操作,发现不能通过松弛操作使得从起始顶点 0 到顶点 4 的路径更短,所以保持原有最短路径不变至此,列表中不存在未标识顶点,Dijkstra 算法结束,找到了一棵以顶点 0 为根的最短路径树
Dijkstra 算法的过程总结:
第一步:从起始顶点开始
第二步:对当前顶点进行标识
第三步:对当前顶点的所有相邻顶点依次进行松弛操作
第四步:更新列表
第五步:从列表的未标识顶点中找到当前最短距离最小
的顶点,作为新的当前顶点
第六步:重复第二步至第五步,直到列表中不存在未标识顶点
Dijkstra 算法主要做两件事情:
(1)从列表中找最值
(2)更新列表
显然,借助最小索引堆作为辅助数据结构,就可以非常容易地实现这两件事情
最后,Dijkstra 算法的时间复杂度:O(E*logV)
来2代码:(最短路问题背代码基本就行了)
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为正值。
请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 -1。
输入格式
第一行包含整数 n 和 m。
接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。
输出格式
输出一个整数,表示 1 号点到 n 号点的最短距离。
如果路径不存在,则输出 -1。
数据范围
1≤n≤500,
1≤m≤105,
图中涉及边长均不超过10000。
输入样例:
3 3
1 2 2
2 3 1
1 3 4
输出样例:
3
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 510;
int n, m;
int g[N][N];
int dist[N];
bool st[N];
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] + g[t][j]);
st[t] = true;
}
if (dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
int main(){
scanf("%d%d", &n, &m);
memset(g, 0x3f, sizeof g);
while (m -- ){
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
g[a][b] = min(g[a][b], c);
}
printf("%d\n", dijkstra());
return 0;
}
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为非负值。
请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 -1。
输入格式
第一行包含整数 n 和 m。
接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。
输出格式
输出一个整数,表示 1 号点到 n 号点的最短距离。
如果路径不存在,则输出 -1。
数据范围
1≤n,m≤1.5×105,
图中涉及边长均不小于 0,且不超过 10000。
数据保证:如果最短路存在,则最短路的长度不超过 109。
输入样例:
3 3
1 2 2
2 3 1
1 3 4
输出样例:
3
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
typedef pair<int, int> PII;
const int N = 1e6 + 10;
int n, m;
int h[N], w[N], e[N], ne[N], idx;
int dist[N];
bool st[N];
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
int dijkstra()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
priority_queue<PII, vector<PII>, greater<PII>> heap;
heap.push({0, 1});
while (heap.size())
{
auto t = heap.top();
heap.pop();
int ver = t.second, distance = t.first;
if (st[ver]) continue;
st[ver] = true;
for (int i = h[ver]; i != -1; i = ne[i])
{
int j = e[i];
if (dist[j] > dist[ver] + w[i])
{
dist[j] = dist[ver] + w[i];
heap.push({dist[j], j});
}
}
}
if (dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
int main()
{
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
while (m -- )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c);
}
cout << dijkstra() << endl;
return 0;
}
Bellman-Ford算法
贝尔曼-福特算法(Bellman-Ford)是由理查德·贝尔曼和莱斯特·福特创立的,求解单源最短路径问题的一种算法。它的原理是对图进行V-1次松弛操作,得到所有可能的最短路径。其优于Dijkstra算法的方面是边的权值可以为负数、实现简单,缺点是时间复杂度过高。
Bellman-Ford算法是一种处理存在负权边的单元最短路问题的算法。解决了Dijkstra无法求的存在负权边的问题。 虽然其算法效率不高,但是也有其特别的用处。其实现方式是通过m次迭代求出从源点到终点不超过m条边构成的最短路的路径。一般情况下要求途中不存在负环。但是在边数有限制的情况下允许存在负环。因此Bellman-Ford算法是可以用来判断负环的。
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你求出从 1 号点到 n 号点的最多经过 k 条边的最短距离,如果无法从 1 号点走到 n 号点,输出 impossible。
注意:图中可能 存在负权回路 。
输入格式
第一行包含三个整数 n,m,k。
接下来 m 行,每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。
输出格式
输出一个整数,表示从 1 号点到 n 号点的最多经过 k 条边的最短距离。
如果不存在满足条件的路径,则输出 impossible。
数据范围
1≤n,k≤500,
1≤m≤10000,
任意边长的绝对值不超过 10000。
输入样例:
3 3 1
1 2 1
2 3 1
1 3 3
输出样例:
3
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 510, M = 10010;
struct Edge
{
int a, b, c;
}edges[M];
int n, m, k;
int dist[N];
int last[N];
void bellman_ford()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
for (int i = 0; i < k; i ++ )
{
memcpy(last, dist, sizeof dist);
for (int j = 0; j < m; j ++ )
{
auto e = edges[j];
dist[e.b] = min(dist[e.b], last[e.a] + e.c);
}
}
}
int main()
{
scanf("%d%d%d", &n, &m, &k);
for (int i = 0; i < m; i ++ )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
edges[i] = {a, b, c};
}
bellman_ford();
if (dist[n] > 0x3f3f3f3f / 2) puts("impossible");
else printf("%d\n", dist[n]);
return 0;
}
SPFA算法
解决存在负环的图的单源最短路径,bellman-ford算法是比较经典的一个,但是大家都知道,这个算法的效率并不咋的,因为它只知道要求单源最短路,至多做|v|(j图的结点数)次松弛操作,感觉有点盲目吧,这里介绍一个有西南交通大学段凡丁1994年发明的一个算法即SPFA,很大程度上优化了bellman-ford算法(建议没有学过的,先去了解一下这个算法),算法的时间效率我就不说了,因为我觉得当我们熟悉某个算法之后,分析时间复杂度就没什么问题了,如果盲目的记忆,意义不大。
SPFA算法的精妙之处在于不是盲目的做松弛操作,而是用一个队列保存当前做了松弛操作的结点。只要队列不空,就可以继续从队列里面取点,做松弛操作,想想bellman-ford算法吧,它只知道做|v|次循环就对了。下面讲讲SPFA为什么这样做呢?还是举个例子:
当前源点1在队列里面,于是我们取了1结点来做对图进行松弛操作,显然这个时候2,3结点的距离更新了,入了队列,我们假设他们没入队列,即现在队列已经空了,那么还有没有必要继续做松弛操作呢?显然没必要了啊,因为源点1要到其他结点必须经过2或3结点啊。现在懂了吧。
先讲一下SPFA的大致思想
算法大致流程是用一个队列来进行维护。 初始时将源加入队列。 每次从队列中取出一个元素,并对所有与他相邻的点进行松弛,若某个相邻的点松弛成功,如果该点没有在队列中,则将其入队。 直到队列为空时算法结束。
判断有无负环:如果某个点进入队列的次数超过V次则存在负环(SPFA无法处理带负环的图)
SPFA算法有两个优化算法 SLF 和 LLL: SLF:Small Label First 策略,设要加入的节点是j,队首元素为i,若dist(j)<dist(i),则将j插入队首,否则插入队尾。 LLL:Large Label Last 策略,设队首元素为i,队列中所有dist值的平均值为x,若dist(i)>x则将i插入到队尾,查找下一元素,直到找到某一i使得dist(i)<=x,则将i出对进行松弛操作。 SLF 可使速度提高 15 ~ 20%;SLF + LLL 可提高约 50%。 在实际的应用中SPFA的算法时间效率不是很稳定,为了避免最坏情况的出现,通常使用效率更加稳定的Dijkstra算法。
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 impossible。
数据保证不存在负权回路。
输入格式
第一行包含整数 n 和 m。
接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。
输出格式
输出一个整数,表示 1 号点到 n 号点的最短距离。
如果路径不存在,则输出 impossible。
数据范围
1≤n,m≤105,
图中涉及边长绝对值均不超过 10000。
输入样例:
3 3
1 2 5
2 3 -3
1 3 4
输出样例:
2
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 100010;
int n, m;
int h[N], w[N], e[N], ne[N], idx;
int dist[N];
bool st[N];
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
int spfa()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
queue<int> q;
q.push(1);
st[1] = true;
while (q.size())
{
int t = q.front();
q.pop();
st[t] = false;
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
if (!st[j])
{
q.push(j);
st[j] = true;
}
}
}
}
return dist[n];
}
int main()
{
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
while (m -- )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c);
}
int t = spfa();
if (t == 0x3f3f3f3f) puts("impossible");
else printf("%d\n", t);
return 0;
}
SPFA判负环
bool spfa()
{
memset(dist, 0, sizeof dist);
memset(cnt, 0, sizeof cnt);
memset(st, 0, sizeof st);
queue<int> q;
for (int i = 1; i <= n; i ++ )
{
q.push(i);
st[i] = true;
}
while(q.size())
{
int t = q.front();
q.pop();
st[t] = false;
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if (dist[j] ____ dist[t] ......)
{
dist[j] = dist[t] ......;
cnt[j] = cnt[t] + 1;
if (cnt[j] >= n) return true;
if (!st[j])
{
q.push(j);
st[j] = true;
}
}
}
}
return false;
}
Floyd算法
1.算法原理
Floyd算法是一个经典的动态规划算法,它又被称为插点法。该算法名称以创始人之一、1978年图灵奖获得者、斯坦福大学计算机科学系教授罗伯特·弗洛伊德命名。Floyd算法是一种利用动态规划的思想寻找给定的加权图中多源点之间最短路径的算法,算法目标是寻找从点i到点j的最短路径。
从任意节点i到任意节点j的最短路径不外乎2种可能,1是直接从i到j,2是从i经过若干个节点k到j。所以,算法假设Dis(i,j)为节点u到节点v的最短路径的距离,对于每一个节点k,算法检查Dis(i,k) + Dis(k,j) < Dis(i,j)是否成立,如果成立,证明从i到k再到j的路径比i直接到j的路径短,便设置Dis(i,j) = Dis(i,k) + Dis(k,j),这样一来,当遍历完所有节点k,Dis(i,j)中记录的便是i到j的最短路径的距离。
2.算法内容
3.算法步骤
4.代码
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,边权可能为负数。
再给定 k 个询问,每个询问包含两个整数 x 和 y,表示查询从点 x 到点 y 的最短距离,如果路径不存在,则输出 impossible。
数据保证图中不存在负权回路。
输入格式
第一行包含三个整数 n,m,k。
接下来 m 行,每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。
接下来 k 行,每行包含两个整数 x,y,表示询问点 x 到点 y 的最短距离。
输出格式
共 k 行,每行输出一个整数,表示询问的结果,若询问两点间不存在路径,则输出 impossible。
数据范围
1≤n≤200,
1≤k≤n2
1≤m≤20000,
图中涉及边长绝对值均不超过 10000。
输入样例:
3 3 2
1 2 1
2 3 2
1 3 1
2 1
1 3
输出样例:
impossible
1
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 210, INF = 1e9;
int n, m, Q;
int d[N][N];
void floyd()
{
for (int k = 1; k <= n; k ++ )
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= n; j ++ )
d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}
int main()
{
scanf("%d%d%d", &n, &m, &Q);
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= n; j ++ )
if (i == j) d[i][j] = 0;
else d[i][j] = INF;
while (m -- )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
d[a][b] = min(d[a][b], c);
}
floyd();
while (Q -- )
{
int a, b;
scanf("%d%d", &a, &b);
int t = d[a][b];
if (t > INF / 2) puts("impossible");
else printf("%d\n", t);
}
return 0;
}
本文作者:PassName
本文链接:https://www.cnblogs.com/spaceswalker/p/15884800.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步