目录
前言
在学习这几个算法之前,我们首先要知道,这些算法是用来求最短路的,那什么是最短路呢,最短路就是,一个图中,一个点到另外一个点所经过边的权重之和最小的路径就是最短路。了解了这个之后,我们还要知道最短路问题分为单源最短路,求图中一个点到其他点的最短路;多元最短路,求图中任意两个点的最短路。
在学习这几个算法之前,我们还要了解两个数据结构,邻接矩阵和邻接表,这两个都是用来描述图的,解释了图中顶点之间的关系。
1.邻接矩阵,邻接矩阵用一个二维数组来表示,每一行每一列代表了一个点,行列的交点就是他们所连的边。而这个二维数组存储的值,可以判断这两个点之间是否存在边, 也可以用来存储他们边的权重。他在查找两个点之间的关系时时间复杂度是 o(1) ,速度还是很快的。由于邻接矩阵是二维数组,如果说在组成的图非常稀疏的情况下,用邻接矩阵来存储图中顶点之间的关系时,就会造成二维数组比较大,我们所需空间就会增大。所以说邻接矩阵用来存储稠密图的效果会更好。
2.邻接表,邻接表是由单链表组成的,每个顶点都会形成一个单链表,单链表的长度就是他所连边的数量。再添加边和删边的时候,时间复杂度是 o(1) ,我们在查找两个点之间的关系时,所需时间复杂度是o(v) (v是链表中点的数量)。如果说图比较稠密,即一个点所连边数较多,那么他的查找速度就会减慢很多,所以说邻接表更适合用来存储稀疏图。
1.dijkstra算法
适用范围 :dijkstra算法通常用于求单源最短路问题,同时图中不能存在负权边,朴素版的dijkstra通常用于求稠密图,而堆优化版的则一般用来求稀疏图。
1.1 朴素版dijkstra
算法介绍:dijkstra算法是基于贪心和动态规划的思想,我们创建一个dist数组来存储源点到其他点的距离,用st数组来存储已经找到最短路径的点。初识时,源点距离初始化为0,而其余的则被设置为无穷远,从原点s出发,它能够直接到达的点就会被设置为 w(s, m),并存储到dist数组中,不能到达的点依旧为无穷远。
再从dist数组中选取距离最小的,那么这个就是源点到顶点的最短路径。然后我们就可以把这个点添加到st数组中,这样就已经找到了一个点,接着从这个点出发,判断这个点到其他点的距离是不是比直接到这个点的距离要小,如果是的话,就把这个点到源点的距离更新,如果不是的话,就保持不变,同时吧他添加到st数组中。后续的点就重复上述操作,直到找到所有点为止。
时间复杂度:朴素版dijkstra需要迭代n次,时间复杂度为o(n^2)。
下面我用一个样例来进行解释
第一次从源点A出发,可以把他到他所连点的距离更新,dist数组可以变化为:
0 | 30 | 10 | 50 |
接着选取A点到其他点的最短距离,即为C点,从这个点出发找到他可以到达的点,从图中判断出,从A直接到D点要比从A到C再到D点的距离要大,所以说从A到D点的距离就会被更新为20,此时dist数组为:
0 | 30 | 10 | 20 |
我把相应代码放到这里:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
constexpr int N = 510;
int g[N][N]; // 每条边的权重
int n, m;
int dist[N]; // 每个点到起点的距离
bool st[N]; // 判断这个点是否已经确定最短路
int dijkstra() {
memset(dist, 0x3f, sizeof(dist));
dist[1] = 0; // 初始化1点距离为0
// 迭代 n 次
for (int i = 0; i < n; i++) {
int t = -1; // 不在st中的,距离最近的点
for (int j = 1; j <= n; j++) {
if(!st[j] && (t == -1 || dist[t] > dist[j])) {
t = j;
}
}
st[t] = true; // 将t放到st数组中
// 用t来更新其他所有点的距离
for (int j = 1; j <= n; j++) {
dist[j] = min(dist[j], dist[t] + g[t][j]);
}
}
if (dist[n] == 0x3f3f3f3f) {
return -1;
} else {
return dist[n];
}
}
int main() {
#ifndef DEBUG
ios::sync_with_stdio(false);
cin.tie(nullptr);
#endif // DEBUG
memset(g, 0x3f, sizeof(g));
cin >> n >> m;
int a, b, c;
for (int i = 0; i < m; i++) {
cin >> a >> b >> c;
g[a][b] = min(g[a][b], c);
}
cout << dijkstra() << "\n";
return 0;
}
1.2 堆优化版dijkstra
算法介绍: 堆优化版的dijkstra算法是在朴素版的基础上进行了优化,我们发现要想找到数组dist最小的节点,是需要o(n)的时间复杂度去遍历所有的点,而这一个操作也是朴素版dijkstra时间复杂度为o(n^2)的主要原因。在里面用了一个最小堆,朴素算法一个关键的比较就是判断这个点到下一个点的距离是不是比当前点到下一个点的距离要小,如果有,这个点的距离就会被更新。而只有被更新过后的点再去到下一个点时,他的距离才有可能会变小,如果没有被更新,那么他就不会减小,根据这个特点,我们可以使用优先队列来存储每一次被更新的点,同时,他的队头元素永远都是距离最小的那个,当队列为空时,说明已经没有点被更新了,说明我们已经找到所有点的最短路。
时间复杂度:在堆优化版的版本中,我们不需要去查找哪个点是已经找到的距离源点的最短点,他的时间复杂度达到了o(1) , 这个版本的总共需要遍历 m 条边,插入数据修改小根堆的时间复杂度为o(lgn), 总的时间复杂度就是o(nlgn).
我把相应代码放在这里:
链式向前星:
#include <iostream>
#include <algorithm>
#include <cstring>
#include <vector>
#include <queue>
using namespace std;
typedef pair<int, int> PII;
constexpr int N = 150010;
int n, m;
int h[N], e[N], ne[N], idx, w[N];
int dist[N]; // 每个点到起点的距离
bool st[N]; // 判断这个点是否已经确定最短路
void add(int a, int b, int c) {
e[idx] = b;
ne[idx] = h[a];
w[idx] = c;
h[a] = idx;
idx++;
}
int dijkstra() {
priority_queue<PII, vector<PII>, greater<PII>> heap; // 使用最小堆时,队头元素永远是最小的
//由于排列时是根据距离进行排序的,所以说,第一个元素一定为距离,第二个元素一定为节点
memset(dist, 0x3f, sizeof(dist));
dist[1] = 0; // 初始化1点距离为0
heap.push({0, 1});
// 队列不为空时
while (heap.size()) {
auto t = heap.top();
heap.pop();
int ver = t.second;
int 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] > (distance + w[i])) {
dist[j] = distance + w[i];
heap.push({dist[j], j});
}
}
}
if (dist[n] == 0x3f3f3f3f) {
return -1;
} else {
return dist[n];
}
}
int main() {
#ifndef DEBUG
ios::sync_with_stdio(false);
cin.tie(nullptr);
#endif // DEBUG
memset(h, -1, sizeof(h));
cin >> n >> m;
int a, b, c;
for (int i = 0; i < m; i++) {
cin >> a >> b >> c;
add(a, b, c); // 往邻接表中插入边
}
cout << dijkstra() << "\n";
return 0;
}
vector
#include <bits/stdc++.h>
using namespace std;
#define int long long
constexpr int N = 1e6 + 10;
typedef pair<int, int> PII;
int n, m;
int h[N], e[N], ne[N], idx, w[N];
int dist[N];
bool st[N];
vector<PII> v[N];
void dijkstra() {
priority_queue<PII, vector<PII>, greater<PII>> heap;
memset(dist, 0x3f, sizeof(dist));
dist[1] = 0;
heap.push({0, 1});
while (heap.size()) {
auto t = heap.top();
heap.pop();
int ver = t.second;
int distance = t.first;
if (st[ver]) {
continue;
}
st[ver] = true;
for (int i = 0; i < v[ver].size(); i++) {
int j = v[ver][i].first;
if (dist[j] > (distance + v[ver][i].second)) {
dist[j] = distance + v[ver][i].second;
heap.push({dist[j], j});
}
}
}
}
void solve() {
memset(h, -1, sizeof(h));
cin >> n >> m;
int a, b, c;
for (int i = 0; i < m; i++) {
cin >> a >> b >> c;
v[a].push_back({b, c});
v[b].push_back({a, c});
}
dijkstra();
cout << dist[n] << "\n";
}
int32_t main() {
#ifndef DEBUG
ios::sync_with_stdio(false);
cin.tie(nullptr);
#endif // DEBUG
int t = 1;
//cin >> t;
while (t--) {
solve();
}
}
2.bellman_ford算法
适用范围:bellman_ford算法用于解决单源最短路问题,他与dijkstra算法不同的是,他可以解决负权边的情况, 在存在负权回路的情况下,就会有一条路走过的边超过n- 1条,所以这个算法也可以判断路径中是否存在负权回路。
算法介绍:bellman_ford算法是对所有边进行 n - 1 轮松弛操作(松弛操作是指对于每个顶点v∈V,都设置一个属性d[v],用来描述从源点s到v的最短路径
上权值的上界,称为最短路径估计),一个图里每两个顶点之间最多有n - 1 条边。经过第一轮松弛操作后,可以得到从源点出发到达目标点最多经过1条边的最短路径,经过第二轮可以得到从源点出发最多经过两条边的最短路径.........经过n -1轮可以得到最多经过n-1条边所经过的最短路径。
时间复杂度:需要经过n- 1次松弛操作,每次操作需要遍历每一条边,时间复杂度为o(nm)
bellman_ford算法核心代码:
for (int i = 0; i < m ; i++) {
int a = edges[i].a;
int b = edges[i].b;
int w = edges[i].w;
dist[b] = min(dist[b], backup[a] + w);
}
用dist数组记录从源点到其他点的距离,初始化源点到各个点的距离为无穷大,第一轮松弛操作之后更新数组为
0 | 10 | ∞ | 10 | ∞ | 30 | ∞ |
经过第二轮松弛操作,数组更新为
0 | 10 | 30 | 10 | 50 | 20 | 50 |
经过第三轮松弛操作后,数组更新为
0 | 10 | 30 | 10 | 40 | 20 | 40 |
...........经过几轮迭代之后,距离已经不再发生变化,图中源点到其他点的最短路径已经确定。
我将代码放在这里供大家参考:
#include <iostream>
#include <cstring>
using namespace std;
constexpr int N = 510, M = 10010;
int dist[N]; // 存储每个点到一号点的距离
int backup[N]; // 记录每次迭代之后各个点到1号点的距离
int n, m, k;
// 存储每条边
struct edge{
int a;
int b;
int w;
}edges[M];
int bellman_ford() {
memset(dist, 0x3f, sizeof(dist)); // 初始化距离为无穷大
dist[1] = 0;
for (int i = 0; i < n - 1; i++) {
memcpy(backup, dist, sizeof(dist)); // 备份每次迭代结果
for (int i = 0; i < m ; i++) {
int a = edges[i].a;
int b = edges[i].b;
int w = edges[i].w;
dist[b] = min(dist[b], backup[a] + w);
}
}
// 比如第n-1个点距离是无穷大,而第 n-1 个点到第 n 个点的距离为负值,那么它到第 n 个点距离就会被更新,可能就不是无穷大
if (dist[n] > 0x3f3f3f3f / 2) {
return -1;
} else {
return dist[n];
}
}
int main() {
cin >> n >> m;
for (int i = 0; i < m; i++) {
cin >> edges[i].a >> edges[i].b >> edges[i].w; // 存入每条边
}
int t = bellman_ford();
if (dist[n] > 0x3f3f3f3f / 2) {
cout << "impossible" << "\n";
} else {
cout << t << "\n";
}
return 0;
}
3.spfa算法
适用范围:spfa算法和bellman_ford算法一样,都用来解决单源最短路存在负权边的问题。
算法介绍:spfa算法使用邻接表来存储每一条边。对于bellman_ford算法每次迭代,不一定都能使他的最短路更新,这就使得他的时间复杂度极大增加,spfa就在此基础上进行了使用宽搜进行优化,使时间复杂度降低,他和dijkstra算法比较像的一点是,他也用了一个先进先出的队列来保存带优化的节点,每次取出队头那个待优化节点进行松弛操作,如果存在点的距离更新,而且这个点不在队列中,就将其存储在队列里面,直到队列变为空。不同的是,dijkstra算法每次需要他走过点到源点最短距离,他要用到优先队列,而spfa只要保证队列里面有数据就行。
时间复杂度:一般情况下:每次操作的时间复杂度是o(1),需要遍历m条边,总时间复杂度是o(m),在一些特殊情况下,时间复杂度可能到达o(nm)。
在spfa算法中可能会存在负权回路(绕一圈之后发现自己的距离变为负数,使得到节点距离降低,这就会使的不断地在这个回路里去走),导致到达别的点的距离变为负无穷。
负权回路举例:
下面给出一个例子:
代码放在下面:
#include <iostream>
#include <algorithm>
#include <cstring>
#include <vector>
#include <queue>
using namespace std;
typedef pair<int, int> PII;
constexpr int N = 1e5 + 10;
int n, m;
int h[N], e[N], ne[N], idx, w[N];
int dist[N]; // 每个点到起点的距离
bool st[N]; // 判断这个点是否在队列里
// 将边添加到邻接表中
void add(int a, int b, int c) {
e[idx] = b;
ne[idx] = h[a];
w[idx] = c;
h[a] = idx;
idx++;
}
int spfa() {
queue<int> q;
memset(dist, 0x3f3f, sizeof(dist));
dist[1] = 0; // 初始化1点距离为0
q.push(1);
st[1] = true;
// 如果队列不空,说明有更新的点使得路径较短
while (q.size()) {
auto 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() {
#ifndef DEBUG
ios::sync_with_stdio(false);
cin.tie(nullptr);
#endif // DEBUG
memset(h, -1, sizeof(h));
cin >> n >> m;
int a, b, c;
for (int i = 0; i < m; i++) {
cin >> a >> b >> c;
add(a, b, c);
}
if (spfa() == 0x3f3f3f3f) {
cout << "impossible" << "\n";
} else {
cout << spfa() << "\n";
}
return 0;
}
4.Floyd算法
适用范围:Floyd算法用来解决多源最短路问题
算法介绍: 1.floyd算法是一个基于动态规划,求图中任意两点间最短路径的算法。这个算法用邻接矩阵来存储两个点之间的路径,在存储时,如果没有直接相连,就会被初始化为无穷大,而他的最终状态就是两个点的最短路径。
2.从第一个点开始,依次将其加入图中,通过松弛计算,枚举是否有路径长度被更改,而这个枚举操作就是遍历每一个点,进行双重循环,判断是否由于新加入的点使得他的最短路径发生变化。如果距离变小,那么,他们的距离就会被更改。
时间复杂度:Floyd算法需要经过三重循环,时间复杂度为o(n^3)。
下面是松弛操作核心代码:
d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
其中d[i][j]是图中i点到j点的最短距离,d[i][j] + d[i][k]相当于从第i点出发到第j点最多经过k个点,d[i][k]为从第i点到第k点的最短路径。从第i点直接到第j点的最短路径与第i点到第k点,再从第k点到第j点路径去取一个最小值。
我下面举一个例子:
我们把这个图转化为一个二维数组:
1 | 2 | 3 | 4 | 5 | 6 | |
1 | 0 | 1 | ∞ | 5 | ∞ | 9 |
2 | 1 | 0 | 1 | ∞ | 4 | 8 |
3 | ∞ | 1 | 0 | ∞ | 2 | ∞ |
4 | 5 | ∞ | ∞ | 0 | ∞ | 3 |
5 | ∞ | 4 | 2 | ∞ | 0 | 3 |
6 | 9 | 8 | ∞ | 3 | 3 | 0 |
接着就是循环加点,1加入之后,1和4之间就可以连通,他们之间最短路径更新为6,接着把2号点加入其中,1号点和3号点也可以连通起来,路径发生更新。
1 | 2 | 3 | 4 | 5 | 6 | |
1 | 0 | 1 | ∞ | 6 | ∞ | 9 |
2 | 1 | 0 | 2 | ∞ | 4 | 8 |
3 | ∞ | 2 | 0 | ∞ | 2 | ∞ |
4 | 5 | 6 | ∞ | 0 | ∞ | 3 |
5 | ∞ | 4 | 2 | ∞ | 0 | 3 |
6 | 9 | 8 | ∞ | 3 | 3 | 0 |
后面就重复上述操作知道所有点都被加入图中,最后数组被更新为:
1 | 2 | 3 | 4 | 5 | 6 | |
1 | 0 | 1 | 2 | 5 | 4 | 7 |
2 | 1 | 0 | 1 | 6 | 3 | 6 |
3 | 2 | 1 | 0 | 7 | 2 | 5 |
4 | 5 | 6 | 7 | 0 | 6 | 3 |
5 | 4 | 3 | 2 | 6 | 0 | 3 |
6 | 7 | 6 | 5 | 3 | 3 | 0 |
写一下代码:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
constexpr int N = 210, INF = 1e9;
int n, m, q;
int d[N][N];
void floyd() {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
for (int k = 1; k <= n; k++) {
d[j][k] = min(d[j][k],d[j][i] + d[i][k]); // 基于动态规划,从j走到k点,只经过了i个点,那么它的距离就是从就j点走到i点,再从i点走到k点的最小值
}
}
}
}
int main() {
#ifndef DEBUG
ios::sync_with_stdio(false);
cin.tie(nullptr);
#endif // DEBUG
cin >> 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;
}
}
}
for (int i = 0; i < m; i++) {
int a, b, w;
cin >> a >> b >> w;
d[a][b] = min(w, d[a][b]);
}
floyd();
while (q--) {
int a, b;
cin >> a >> b;
if (d[a][b] > INF / 2) {
cout << "impossible" << "\n";
} else {
cout << d[a][b] << "\n";
}
}
return 0;
}
总结:
用一张图来总结一下上面提出的几种算法: