最短路:
一、朴素版dijkstra
合适的使用范围:无负权边的稠密图
时间复杂度: O ( n 2 ) O(n^2) O(n2)
实现方式:按点更新,用当前最近的没有更新到的点更新其他没更新到的点。第一层枚举
n
n
n次,第二层1判断最近的点,第二层2更新其他还没更新到的点。
代码实现:
int dijkstra() {
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
for (int i = 1; i < n; i++) {
int t = -1;
for (int j = 1; j <= n; j++)
if (!st[j] && (t == -1 || dist[j] < dist[t])) t = 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];
}
如果有负权边,则可能使得最短路比原来的长。举例:
1
−
2
−
4
,
1
−
3
−
5
,
2
−
3
−
(
−
3
)
1-2-4,1-3-5,2-3-(-3)
1−2−4,1−3−5,2−3−(−3)。可以发现
1
1
1 到
3
3
3 的最短路为
1
1
1 ,但由于dijkstra的贪心思想,会在还没走
2
−
3
2-3
2−3 这条边之前就将1-2的路径更新了。
二、堆优化版dijkstra
合适的使用范围:无负权边的稀疏图
时间复杂度: O ( m l o g m ) O(mlogm) O(mlogm)
实现方式:与朴素版dijkstra相似,但是,是用最短边去更新没有更新到的点。第一层枚举最短边,第二层更新没有更新到的点。
代码实现:
struct node {
int x, y;
bool operator < (const node &a) const {
return y > a.y;
}
};
int n, m;
int h[N], e[N], w[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));
priority_queue<node> q;
dist[1] = 0;
q.push({1, 0});
while (q.size()) {
node t = q.top();
q.pop();
if (st[t.x]) continue;
for (int i = h[t.x]; i != -1; i = ne[i]) {
if (dist[e[i]] > dist[t.x] + w[i]) {
dist[e[i]] = dist[t.x] + w[i];
q.push({e[i], dist[e[i]]});
}
}
st[t.x] = true;
}
if (dist[n] == 0x3f3f3f3f) return -1;
else return dist[n];
}
同样如果有负权边,则无法判断是否存在最短路。
三、Bellman-ford
合适的使用范围:有负权边且有最短路边数限制的图(可以判断负环)
时间复杂度: O ( n m ) O(nm) O(nm)
实现方式:第一次枚举
n
n
n次,第二次枚举
m
m
m条边,可以证明n次迭代后,如果有最短路必能求出,枚举k次即k条边的最短路。
注意:当用该算法求k条边的最短路时要有备份backup[n],原因在于在一次
n
n
n 的枚举中不能用已经更新过的点去更新其他点。最后判断有无最短路时要用已得距离与一个较大的数进行比较。
#include <cstdio>
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 510, M = 10010;
struct Edge {
int a, b, c;
} 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++)
if (dist[edge[j].b] > backup[edge[j].a] + edge[j].c)
dist[edge[j].b] = backup[edge[j].a] + edge[j].c;
}
if (dist[n] > 0x3f3f3f3f / 2) return -1;
else return dist[n];
}
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);
edge[i].a = a, edge[i].b = b, edge[i].c = c;
}
if (Bellman_ford() == -1) printf("impossible\n");
else printf("%d\n", dist[n]);
return 0;
}
四、SPFA
合适的适用范围:有负权边且没最短路边数的限制的图(可以判断负环)
时间复杂度:一般 O ( m ) O(m) O(m),最坏 O ( n m ) O(nm) O(nm)
实现方式:与Bellman_ford算法相似,不同之处在于要用一个队列来存储改变了值的点,节点的值没变的则不枚举。所以,该算法也与堆优化的dijkstra算法十分相似。
#include <cstdio>
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int N = 1e5 + 10;
int n, m;
int h[N], e[N], w[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() {
memset(h, -1, sizeof(h));
scanf("%d %d", &n, &m);
for (int i = 0; i < m; i++) {
int a, b, c;
scanf("%d %d %d", &a, &b, &c);
add(a, b, c);
}
if (spfa() == 0x3f3f3f3f) printf("impossible\n");
else printf("%d\n", dist[n]);
return 0;
}
五、Floyd
算法实现:动态规划,枚举中间点,中间点首先枚举,可以有负权边,但不能有负环。
时间复杂度: O ( n 3 ) O(n^3) O(n3)
例题
给定一个n个点m条边的有向图,图中可能存在重边和自环,边权可能为负数。
再给定k个询问,每个询问包含两个整数x和y,表示查询从点x到点y的最短距离,如果路径不存在,则输出“impossible”。
数据保证图中不存在负权回路。
#include <cstring>
#include <iostream>
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;
}
六、SPFA判断负环
实现方式:根据SPFA的实现原理我们可以知道当所有的点都无法更新的时候算法就会结束了,但如果有负环的话则会导致程序无法结束。根据抽屉原理我们可以知道,一个有 n n n 个节点的图,如果没出现环的话,那么从一个点到另一个的最短路的最多经过 n − 1 n - 1 n−1个点,所以我们可以根据转移数量来判定一个图中有没有负环。
#include <cstdio>
#include <iostream>
#include <queue>
#include <cstring>
using namespace std;
const int N = 1e5 + 10;
int n, m;
int h[N], e[N], ne[N], w[N], idx;
int dist[N];
bool st[N];
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;
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];
cnt[j] = cnt[t] + 1;
if (cnt[j] >= n) return true;
if (!st[j]) {
st[j] = true;
q.push(j);
}
}
}
}
return false;
}
int main() {
memset(h, -1, sizeof(h));
scanf("%d %d", &n, &m);
for (int i = 0; i < m; i++) {
int a, b, c;
scanf("%d %d %d", &a, &b, &c);
add(a, b, c);
}
if (spfa()) printf("Yes\n");
else printf("No\n");
return 0;
}
七、小结
今天的内容看起来很少,但是含量很足,每种算法都是求最短路的,但它们之前的使用范围和复杂度都是不同的,之后还要多刷题巩固。