常见的最短路模型
源点: 起点
汇点: 终点
单源最短路: 从1个点到其他所有点的最短路, 比如求1号点到第n号点的最短路
多源汇最短路 : 起点和终点不确定
最短路{单源最短路{所有边权重为正数{朴素Dijkstra O(n2) 比较适合稠密图,边数m=n2比较多堆优化版Dijkstra O(mlogn) 稀疏图,m=n≤105,n2会超时,用堆优化存在负权边{Bellman_Ford O(nm) n是点数,m是边数(经过不超过k条边,只能用此方法) SPFA 一般O(m),最坏O(nm) 多源汇最短路 Floyd算法 O(n3)
最短路\left\{\begin{matrix}
单源最短路\left\{\begin{matrix}
所有边权重为正数\left\{\begin{matrix}
朴素Dijkstra O(n^2) 比较适合稠密图, 边数m = n ^2比较多 \\
堆优化版Dijkstra O(mlogn) 稀疏图, m = n \leq 10^5, n^2会超时,用堆优化
\end{matrix}\right. \\
存在负权边\left\{\begin{matrix}
Bellman\_Ford O(nm) n是点数,m是边数(经过不超过k条边,只能用此方法) \\
SPFA 一般O(m), 最坏O(nm)
\end{matrix}\right.
\end{matrix}\right. \\
多源汇最短路 Floyd算法 O(n^3)
\end{matrix}\right.
最短路⎩⎪⎪⎪⎪⎨⎪⎪⎪⎪⎧单源最短路⎩⎪⎪⎨⎪⎪⎧所有边权重为正数{朴素Dijkstra O(n2) 比较适合稠密图,边数m=n2比较多堆优化版Dijkstra O(mlogn) 稀疏图,m=n≤105,n2会超时,用堆优化存在负权边{Bellman_Ford O(nm) n是点数,m是边数(经过不超过k条边,只能用此方法) SPFA 一般O(m),最坏O(nm) 多源汇最短路 Floyd算法 O(n3)

最短路 比较难的 是建图
各个算法的原理简介
dijkstra 原理是 贪心
floyd 基于动态规划
bellman_ford 基于离散数学的知识
Dijkstra 算法
用来解决 单源最短路问题
朴素版Dijkstra
s集合 : 当前已经确定最短距离的点
1.dist[1] = 0, dist[i] = INF;
2.for (int i = 0; i < n; i ++ ){
// 每次循环出队一个元素,可以确定一个点的最短距离
// 循环n次, 可以确定n个点的最短距离
t <- 找到不在s中的最短距离的点
s <- t (t加入到s集合中)
用t更新其他所有点的距离(用t的出边去更新其他所有点的距离,
比如t->x, 看下1~x的距离能否用 1~t + t~x的距离更新
)即: dist[x] > dist[t] + w 的话, dist[x] = dist[t] + w;
}

例子
绿颜色表示确定了最短路的点
红颜色表示距离待定的点
初始状态:

第1次 发现2号点和3号点到1的距离可以更新

第二次寻找不在s中的最短距离的点, 2号点确定为绿色

用2来更新出边到1的距离, 可以将3号点的距离更新成3, 此时这轮迭代结束

下轮迭代只剩一个点, 可以直接确定.

时间复杂度O(n^2)
外层循环n次, 内层循环找不在s中的距离最近的点n次, 用t更新其他点n次.
总共n^2
AcWing 849. Dijkstra求最短路 I
分析
裸题
正权自环, 显然不会出现在最短路中,忽略
重边, 直接对g[a][b] = min(g[a][b], c);
找不在s中距离最短的点
int dijkstra(){
for (int i = 0; i < n; i ++ ){
int t = -1;
for (int j = 1; j <= n; j ++ )
if (!st[j] && (t == - 1 || dist[t] > dist[j])) t = j;
// !st[j] 不在s中
// t == -1表示第一次, dist[t] > dist[j] 找最小
...
st[t] = true; // 标记成绿色
}
}
代码
#include <iostream>
#include <cstring>
using namespace std;
const int N = 510, M = 10010;
int g[N][N];
int n, m;
bool st[N];
int dist[N];
int dijkstra(){
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
//st[1] = true; 一定不能加这句话.
for (int i = 0; i < n; i ++ ){
// 找不在s中最短距离的点
int t = -1;
for (int j = 1; j <= n; j ++ ){
if (!st[j] && (t == -1 || dist[t] > dist[j])) t = j; // 注意 !st[j] && ( || )
}
st[t] = true;
for (int j = 1; j <= n; j ++ )
dist[j] = min(dist[j], dist[t] + g[t][j]);
}
if (dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
int main(){
cin >> n >> m;
memset(g, 0x3f, sizeof g);
while (m -- ){
int a, b, c;
cin >> a >> b >> c;
g[a][b] = min(g[a][b], c);
}
int t = dijkstra();
cout << t << endl;
return 0;
}
堆优化版dijkstra
如果是个稀疏图, n=105n = 10^5n=105, n2n^2n2时间必爆
优化
找不在s中的点, 外部for循环n次, 找点n次,总共n^2s = t, 每次取1个点, 外部循环n次, 总共n 用t更新其他点的距离, 外部循环n次, 总共m`条边, 因此是m.
所以第一步可以用堆来找最小值使得
每次找最小值O(1).
第二步总共仍然是n
而第三步在堆中寻找元素时间复杂度O(logn), 总共修改m次, 所以总共mlognmlognmlogn次
因此堆优化dijkstra时间复杂为O(mlogn)

实现堆的方式
手写
麻烦.
STL
STL不支持修改堆中元素操作, 堆中可能存在冗余.
比如存了dist[1] = 10, 又存了dist[1] = 15;
直接用st数组判重即可, 如果当前出队的元素, 已经确定为绿色,直接扔掉
AcWing 850. Dijkstra求最短路 II
分析
堆优化dijkstra 裸题
代码
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int N = 1e6 + 10;
typedef pair<int, int> PII;
int h[N], e[N], w[N], ne[N], idx;
int n, m;
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; 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 算法
for循环n次 一定要对原来的距离备份,防止串连
for 所有边 a, b, w. a⟶wba\stackrel{w}{\longrightarrow}ba⟶wb (从a到b的边,权重为w)(边的存储方式不一定写成邻接表, 可以是结构体)
dist[b] = min(dist[b], dist[a] + w);

串联问题(备份)

比如截图中右上角的问题
边权重1-2 权重1, 2-3 权重1, 1-3权重3, 求1到3 最多经过1条边的最短路.
应该是3.
但是如果不备份
点 1 2 3
dist 0 ∞\infty∞ ∞\infty∞
for 循环第1次
第二层循环(假如先枚举1-2这条边)
2号点距离更新成1
点 1 2 3
dist 0 1 ∞\infty∞
然后再枚举2-3(这时2到1的距离已经发生变化),因此3-1的距离就变成dist[2] + w[2,3] = 1 + 1 = 2
点 1 2 3
dist 0 1 2
虽然最外层只迭代了1次, 但更新过程中发生了串联, 将已经更新点,又拿来作为条件,更新后面的点.
如何不发生串联呢
保证更新的时候只用最外层循环for上一次的结果, 即:在最外层循环中+备份

使用上一次备份更新, 那么3的距离变成 ∞+1\infty + 1∞+1,不会变成2
负权回路
图中存在负权回路的话,最短路不一定存在

可以求出图中是否存在负权回路
最外层循环次数的意义, 比如迭代k次, 表示经过不超过k条边的最短距离,
如果第n次迭代的时候,又更新了某些边, 说明:存在一条最短路, 这条最短路上的边数 >= n.
n条边的话,意味这这条路径上有n + 1个点, 因为总的点数只有n,因此这条路径上必定有两个点相同,即:这条路径上必定存在环.
路径上存在环, 并且可以更新当前点的距离,那么一定是负环.
因此,第n迭代的时候,有更新的话,说明存在边数为n的最短路径,说明存在负环
一般找负环用spfa,这里值是提及下,bellman-ford可以用来找负环.
时间复杂度O(nm)
两重循环,外层n次,内层循环所有边数m
有负权回路有些情况可以求得最短路
比如2号点的负权回路,不在最短路径上. 但是, spfa算法要求图中任何点,不能包含负环

AcWing 853. 有边数限制的最短路
分析
题目说了边权可能为负数,因此一定不能用dijkstra
题目要求最多经过k条边, 有负环也无所谓了
代码
if (dist[n] > 0x3f3f3f3f / 2 ) return -1;

(1号点当前不能到5号点)
(1号点当前也不能到n号点)
但是5号点能更新1号点到5号点的距离
dist[n] <= dist[5] + w[5, n]
n号点,最终可能到不了,但是比正无穷小一点点,也算到不了
代码
#include <iostream>
#include <cstring>
using namespace std;
const int N = 510, M = 100010 * 2 + 10;
struct Edge{
int a, b, w;
}edge[M];
int n, m, k;
int dist[N], backup[N];
int bellman_ford(){
memset(dist, 0x3f, sizeof dist);
dist[1] = 0; // 注意初始化
for (int i = 0; i < k; i ++ ){
memcpy(backup, dist, sizeof dist);
for (int j = 0; j < m; j ++ ){
int a = edge[j].a, b = edge[j].b, c = edge[j].w;
if (dist[b] > backup[a] + c) // 一定要是 backup[a] + c
dist[b] = backup[a] + c;
}
}
if (dist[n] > 0x3f3f3f3f / 2) return -1;
return dist[n];
}
int main(){
cin >> n >> m >> k;
for (int i = 0; i < m; i ++ ){
scanf("%d%d%d", &edge[i].a, &edge[i].b, &edge[i].w);
}
int t = bellman_ford();
if (t == -1) puts("impossible");
else cout << t << endl;
return 0;
}
SPFA 算法
只要没有负环,就可以用
spfa是对bellman_ford算法的优化
如果被spfa被出题人卡了,只能换堆优化版dikstra

因为每次进行dist[b] = min(dist[b], dist[a] + w)不一定会更新距离, spfa就是对这一步进行优化.
只有当dist[a]变小了,dist[b]才会变小.
优化方式
用宽搜进行优化
队列里存的就是能让dist[b]变小的节点,比如上图中的a
queue ⟵\longleftarrow⟵ 1(起点)
while queue 不空
1. t ⟵\longleftarrow⟵ q.front(); q.pop();
2. 更新下t的所有出边, t⟶wbt\stackrel{w}{\longrightarrow}bt⟶wb (t即上图中的a, 因为t变小,所以出边的距离b才会变小)
queue ⟵\longleftarrow⟵ b (如果队列中有b的话,就不用重复加入了)
基本思路: 更新过谁,让再拿更新过的点去更新出边, 只有我变小了,我的出边才会变小.

时间复杂度 最坏情况下O(nm)O(nm)O(nm)
因为是对bellman_ford算法的改进, 最坏情况下O(nm)O(nm)O(nm), 相当于队列优化没起任何作用
AcWing 851. spfa求最短路
分析
spfa代码与dijkstra相似, 直接copy过来,改下dijkstra函数即可
代码
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int N = 1e6 + 10;
int h[N], e[N], w[N], ne[N], idx;
int n, m;
int dist[N];
bool st[N];
typedef pair<int, int> PII;
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; // st数组表示当前点是否在队列中
while (q.size()){
auto 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] + w[i]){
dist[j] = dist[t] + w[i];
if (!st[j]){ // 只有当j不在队列中才加入, 防止重复加入j
q.push(j);
st[j] = true;
}
}
}
}
if (dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
int main(){
cin >> 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 == -1) puts("impossible");
else cout << t << endl;
return 0;
}
code(循环队列写法)
#include <iostream>
#include <cstring>
using namespace std;
const int N = 100010, M = 100010;
int h[N], e[M], w[M], ne[M], idx;
int n, m;
int dist[N], st[N];
int q[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;
int hh = 0, tt = 1;
q[0] = 1;
st[1] = true;
while (hh != tt){
auto t = q[hh ++ ];
if (hh == N) hh = 0;
st[t] = false;
for (int i = h[t]; ~i; i = ne[i]){
int j = e[i];
if (dist[j] > dist[t] + w[i]){
dist[j] = dist[t] + w[i];
if (!st[j]) {
st[j] = true;
q[tt ++ ] = j;
if (tt == N) tt = 0;
}
}
}
}
if (dist[n] > 0x3f3f3f3f / 2) return -1;
return dist[n];
}
int main(){
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
for (int i = 0; i < m; i ++ ) {
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c);
}
int t = spfa();
if (t == -1) puts("impossible");
else cout << t << endl;
return 0;
}
SPFA求负环
dist[x]:当前1~x的最短距离
cnt[x]:当前最短路的边数
每次更新的时候
dist[x] = dist[t] + w[i];
cnt[x] = cnt[t] + 1; // 从1到t的边 + 1条边
如果cnt[x] >= n, 意味着当前最短路经过n条边, 那么一定有n + 1个点, 而图中最多才n个点, 一定有两个点相同,且出现在最短路径中, 比如i出现两次, 即 路径中存在环, 并且能更新距离, 所以是负环


AcWing 852. spfa判断负环
分析
直接将spfa算法,稍加改进即可.
可以删掉memset(dist, 0x3f, sizeof dist); dist[1] = 0, 因为求的不是距离的绝对值,而是距离之间的相对值.
队列开始的时候, 需要将所有点加入进来(不仅仅是1号点), 因为题目判断的是是否存在负环, 不是是否存在以1为起点的负环
加入
int spfa(){
for (int i = 1; i <= n; i ++) {
q.push(i);
st[i] = true;
}
while (q.size()){
cnt[j] = cnt[t] + 1;
if (cnt[j] >= n) return true;
.....
}
return false;
}
int main(){
...
if (spfa()) puts("YES");
else puts("NO");
}
代码
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int N = 1e6 + 10;
int h[N], e[N], w[N], ne[N], idx;
int n, m;
int d[N];
bool st[N];// st数组表示当前的点是否在队列中,如果在队列中就不需要再入队了
int cnt[N];// 记录更新次数
void add(int a, int b, int c){
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
}
int spfa(){
queue<int> q;
for (int i = 1; i <= n; i ++ ){ // 加入所有点, 因为题目是判断所有点开始是否有负数环
st[i] = true;
q.push(i);
}
while (q.size()){
int t = q.front(); q.pop();
st[t] = false;//出队标记为false
for (int i = h[t]; ~i; i = ne[i]){
int j = e[i];
if (d[j] > d[t] + w[i]){
d[j] = d[t] + w[i];
cnt[j] = cnt[t] + 1;
if (cnt[j] >= n) return true;
if (!st[j]){
st[j] = true;
q.push(j);
}
}
}
}
return false;
}
int main(){
cin >> 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 (spfa()) puts("Yes");
else puts("No");
return 0;
}
Floyd 算法
用来求多源汇最短路
用邻接矩阵来存储d[i, j], 表示i 到 j的距离
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]);
原理(动态规划)
k表示阶段 d[k, i, j]
d[k, i, j] = d[k - 1, i, k] + d[k - 1, k , j];
// 当只经过1 ~ k - 1这些点的时候, 加上第k个点,
// 那么就是先从i ~ k + k ~ j(i, j表示1 ~ k - 1中的点)
// 可以发现第1维 没什么用, 可以去掉
d[i, j] = d[i, k] + d[k, j];
时间复杂度O(n^3)
AcWing 854. Floyd求最短路
分析
裸题
代码
#include <iostream>
#include <cstring>
using namespace std;
const int N = 210, M = 200010, INF = 0x3f3f3f3f;
int dist[N][N];
int n, m, Q;
int 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(){
cin >> n >> m >> Q;
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= n; j ++ )
if (i != j) dist[i][j] = INF;
else dist[i][j] = 0;
for (int i = 0; i < m; i ++ ){
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
dist[a][b] = min(dist[a][b], c);
}
floyd();
while (Q -- ){
int a, b;
cin >> a >> b;
int t = dist[a][b];
if (t > INF / 2) puts("impossible");
else printf("%d\n", t);
}
return 0;
}
if(d[a][b] > INF / 2)
a 与 b不存在通路的情况下, INF会被更新, 可能会比INF小点.
初始化问题
因为多源汇问题, 题目会询问1到1的距离, 所以不能用memset(dist, 0x3f, sizeof dist);去初始化所有点的距离
博客介绍常见最短路模型,包括单源和多源汇最短路。详细阐述Dijkstra、Bellman - Ford、SPFA、Floyd等算法原理、时间复杂度,还结合AcWing题目给出分析与代码。如Dijkstra用于单源正权边,Floyd用于多源汇,Bellman - Ford可处理负权边及判断负环。
&spm=1001.2101.3001.5002&articleId=114165304&d=1&t=3&u=d2c7e2b599bf4188b222d62f2e8e0d67)
369

被折叠的 条评论
为什么被折叠?



