# 最短路
最短路算法的分类:
- 单源最短路
- 所有边权都是正数
- 朴素的 Dijkstra 算法 O(n^2) 适合稠密图
- 堆优化版的 Dijkstra 算法 O(mlog n)(m 是图中节点的个数)适合稀疏图
- 存在负权边
- Bellman-Ford O(nm)
- spfa 一般 O(m),最坏 O(nm)
- 所有边权都是正数
- 多源汇最短路 Floyd 算法 O(n^3)
# 朴素的 Dijkstra 算法
集合 S:当前已经确定最短距离的点
- dist[1] = 0, dist[i] = 正无穷
- for v: 1 ~ n
- t <- 不在 s 中的距离最近的点
- s <- t
- 用 t 更新其他点的距离
朴素的 Dijkstra 算法往往是稠密图,用邻接矩阵来存储
# 算法模板
int g[N][N]; // 存储每条边;为稠密阵所以用邻接矩阵存储
int dist[N]; // 存储1号点到每个点的最短距离
bool st[N]; // 存储每个点的最短路是否已经确定
// 求1号点到n号点的最短路,如果不存在则返回-1
int dijkstra()
{
memset(dist, 0x3f, sizeof dist); //初始化距离 0x3f代表无限大
dist[1] = 0; //第一个点到自身的距离为0
for (int i = 0; i < n-1; i ++) //有n个点所以要进行n-1次迭代;第一个到自身距离为0
{
int t = -1; // 在还未确定最短路的点中,寻找到1号点距离最小的点
for (int j = 1; j <= n; j ++)
if (!st[j] && (t == -1 || dist[t] > dist[j]))
t = j;
st[t] = true; // t号点的最短路已经确定
// 用t更新其他点的距离
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];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
时间复杂是 O(n2+m), n 表示点数,m 表示边数
# 题目
- AcWing849. Dijkstra 求最短路 I
# 堆优化版的 Dijkstra 算法
集合 S:当前已经确定最短距离的点
- dist[1] = 0, dist[i] = 正无穷
- for v: 1 ~ n
- t <- 不在 s 中的 与起始点距离最近的点 ;小顶堆维护 O(logN)
- s <- t; O(1)
- 用 t 更新其他点的距离 ; O(mlogN)
稀疏图用堆优化版的 Dijkstra 算法
# 时间复杂度
O(mlogN)
# 代码实现
堆优化版的 Dijkstra 算法有点像宽搜
class Solution {
public:
int networkDelayTime(vector<vector<int>>& times, int N, int K) {
const int INF = 0x3f3f3f3f;
typedef pair<int, int> PII; // first:距离; second: 几号点
vector<bool> st(N+1, false); // 是否已得到最短距离
vector<int> dist(N+1, INF); // 距离起始点的最短距离
unordered_map<int, vector<PII>> graph; // 邻接表;u->v,权重w
priority_queue<PII, vector<PII>, greater<PII>> heap; // 小顶堆;维护到起始点的最短距离和点
for (auto &t: times){ // 初始化邻接表
graph[t[0]].push_back({t[2],t[1]});
}
heap.push({0, K});
dist[K] = 0;
while(heap.size()){
auto t = heap.top();
heap.pop();
int ver = t.second, distance = t.first;
if (st[ver]) continue; // 之前更新过,是冗余备份
st[ver] = true;
for (auto &p: graph[ver]){
if (dist[p.second] > distance + p.first){ // 用t去更新其他点到起始点的最短距离
dist[p.second] = distance + p.first;
heap.push({dist[p.second], p.second});
}
}
}
int ans = 0;
for (int i = 1; i<=N; i++){
ans = max(ans, dist[i]);
}
if (ans == INF) return -1;
return ans;
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# Bellman-Ford 算法
# 算法思路
n 个点,m 条边
- 循环 n 次
- 遍历所有边u->v,权 w (松弛操作)
- dist[v]=min(dist[v], dist[u]+w)
- 遍历所有边u->v,权 w (松弛操作)
# 应用
- 处理有负权边的图
- 循环次数的含义:循环 K 次后,表示不超过 K 条边的最短距离
- 有边数限制的最短路,只能用 Bellman-Ford 算法,不能用 spfa 算法
- 如果有负权回路,最短路不一定存在
- Bellman-Ford 算法可以求出是否有负环
- 第 n 循环后,还有更新,说明路径上有 n+1 个点,也就是存在环,还有更新,说明环是负环
- 循环 n 次后, 所有的边
u->v,权w
满足三角不等式:dist[v]<=dist[u]+w
# 代码实现
Bellman-Ford 算法
class Solution {
public:
int findCheapestPrice(int n, vector<vector<int>>& flights, int src, int dst, int K) {
const int INF = 0x3f3f3f3f;
vector<int> dist(n, INF); // 到起点的最短距离
vector<int> backup(n); // 防止串联
dist[src] = 0;
for (int i = 0; i<= K; i++){ // 松弛K次
backup.assign(dist.begin(), dist.end());
for (auto &f: flights){ // 枚举所有边
dist[f[1]] = min(dist[f[1]], backup[f[0]] + f[2]); // 更新最短路
}
}
if (dist[dst] > INF /2) return -1;
return dist[dst];
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# spfa 算法
在各个方面都好于 Bellman-Ford 算法
但是不能求有边数限制的最短路
SPFA 算法是单源最短算法中限制最小的算法,只要没有负环,就可以用 SPFA 算法,一般的只要求最短路就不含有负环
SPFA 算法是对 Bellman-Ford 算法的优化
# 算法思路
n 个点,m 条边
对 Bellman-Ford 算法进行优化:
- 循环 n 次
- 遍历所有边u->v,权 w (松弛操作)
dist[v]=min(dist[v], dist[u]+w)
; 只有 dist[u]变小了,dist[v]才会变小
- 遍历所有边u->v,权 w (松弛操作)
# spfa 算法步骤
- queue <– 起始点
- while queue 不为空
- t <– 队头
- queue.pop()
- 用 t 更新所有出边 t –> v,权值为 w - queue <– v (若该点被更新过,则拿该点更新其他点)
时间复杂度:
一般:O(m) 最坏:O(nm)
- t <– 队头
# 场景
- 存在负权边,求单源最短路
- spfa 也能解决权值为正的图的最短距离问题,且一般情况下比 Dijkstra 算法还好
- spfa 算法更为通用,在求单源最短路的时候,我们可以先考虑 spfa 算法,如果数据被卡,再考虑实现别的单源最短路算法;一般笔面试题数据都不会被卡,OI,ACM 可能被卡
# 代码实现
spfa 算法
class Solution {
public:
int networkDelayTime(vector<vector<int>>& times, int N, int K) {
const int INF = 0x3f3f3f3f;
vector<int> dist(N+1, INF); // 保存到起点的距离
vector<bool> st(N+1, false); // 是否最短
typedef pair<int, int> PII;
unordered_map<int, vector<PII>> edges; // 邻接表
queue<int> q;
q.push(K);
dist[K] = 0;
st[K] = true; // 已经在队列中
for (auto &t: times){
edges[t[0]].push_back({t[1], t[2]});
}
while (!q.empty()){
auto t = q.front();
q.pop();
st[t] = false;
for (auto &e: edges[t]){
int v = e.first, w = e.second;
if (dist[v] > dist[t] + w){
dist[v] = dist[t] + w;
if (!st[v]){
q.push(v);
st[v] = true;
}
}
}
}
int ans = *max_element(dist.begin()+1, dist.end());
return ans == INF ? -1: ans;
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# spfa 算法求负环
- Acwing 852. spfa 判断负环
# 算法思路
- 增加
cnt[N]
来记录最短路的边数 - 当最短路的边数大于等于 n,可知经过的点大于等于 n+1
- 一共 n 个点,根据抽屉原理可知最短路存在负环
# 代码实现
#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII;
unordered_map<int, vector<PII>> edges; // 邻接表
int n, m; // n个点,m条边
const int N = 2010;
int dist[N]; // 到起始点的最小距离
bool st[N]; // 在队列中是否存在
int cnt[N]; // 记录最短路的边数
bool spfa(){
queue<int> q;
for (int i = 1; i <=n; i++){ // 所有点入队列;负环可能存在在所有点出发的最短路上
q.push(i);
st[i] = true;
}
while (!q.empty()){
int u = q.front();
q.pop();
st[u] = false; // 不在队列
for (auto &e: edges[u]){
int v = e.first, w = e.second;
if (dist[v] > dist[u] + w){
dist[v] = dist[u] + w; // 更新最短路 权值
cnt[v] = cnt[u] + 1; // 更新经过的边数
// 存在负环;边数>=n,经过的点>=n+1;根据抽屉原理得,最短路存在负环
if (cnt[v] >= n) return true;
if (!st[v]){
q.push(v);
st[v] = true;
}
}
}
}
return false;
}
int main(){
cin >> n >> m;
while (m--){ // 构造图
int u, v, w;
cin >> u>> v>> w;
edges[u].push_back({v, w});
}
if (spfa()) puts("Yes");
else puts("No");
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# Floyd 算法
多源汇最短路
- for (k = 1; k<=n ; k++)
- for (i = 1; i<= n; i++)
- for (j = 1; j<=n; j++)
- d[i,j] = min(d[i,j], d[i,k]+d[k,j])
- for (j = 1; j<=n; j++)
- for (i = 1; i<= n; i++)
# 算法原理
floyd 算法是基于动态规划的
d[k, i, j] 表示 从 i 出发,只经过 1~k 到达 j 点的最短距离
# 题目
- AcWing 854. Floyd 求最短路
#include <bits/stdc++.h>
using namespace std;
const int INF = 0x3f3f3f3f;
const int N = 210;
int n, m, k; // n个点,m条边,k次询问
int grid[N][N]; // 图的矩阵存储
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(){
cin >> n >> m >> k;
for (int i = 1; i <=n ; i++){ // 初始化邻接矩阵
for (int j = 1; j<=n; j++){
if (i == j) d[i][i] = 0; // 自己到自己距离0
else d[i][j] = INF; // 到别的点距离正无穷
}
}
int u, v, w;
while (m--){ // m条边
cin >> u >> v >> w;
d[u][v] = min(d[u][v], w); // 重边取最小
}
floyd(); // floyd计算多源最短路
while (k--){ // k次询问
cin >> u >> v;
if (d[u][v] > INF/2) puts("impossible");
// 由于存在负权边,所以比INF/2大,就是不可达
else cout << d[u][v] << endl;
}
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
← 0x61_图 0x64_最小生成树 →