最短路算法

2023秋第十二周最短路

image-20231120112217393

更多有关于最短路的详细证明,可以参考最短路 - OI Wiki (oi-wiki.org)

由于图论题更多在于抽象问题建图的过程,故在此不详细给出原理和证明,侧重讲解实现方法。

最短路的重要性质

对于边权为正的图,任意两个结点之间的最短路,不会经过重复的结点。

对于边权为正的图,任意两个结点之间的最短路,不会经过重复的边。

对于边权为正的图,任意两个结点之间的最短路,任意一条的结点数不会超过 $n$,边数不会超过 $n-1$。

单源最短路

正权图:Dijkstra

对于边 $(u,v)$,松弛操作对应下面的式子:$dis(v) = \min(dis(v), dis(u) + w(u, v))$。

这么做的含义是显然的:我们尝试用 $S \to u \to v$(其中 $S \to u$ 的路径取最短路)这条路径去更新 $v$ 点最短路的长度,如果这条路径更优,就进行更新。

朴素版:常用于稠密图,点数的平方和边数很接近

原理:某个点的最短路,肯定是由某条从起点开始,到达上一个点的最短路更新而来

做法:迭代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
25
void dijkstra(int n, int s) {
// 把dist数组中的每一个字节初始化为0x3f
memset(dist, 0x3f, sizeof(dist));
dist[s] = 0;
for (int i = 1; i <= n; i ++ ) {
// 找到距离起点最近的点,记为t
int t = -1;
for (int j = 1; j <= n; j ++ ) {
if (!st[j] && (t == -1 || dist[t] > dist[j]))
t = j;
}
// 标记t,st[t] = true代表t这个点已经找到了最短路
st[t] = true;

if (t == n) break;
// 现在,距离起点最近的点就是点t
for (int j = 1; j <= n; j ++ ) {
if (g[t][j] == -1) continue;
// 满足三角形不等式,那就更新最短路
if (dist[j] > dist[t] + g[t][j]) {
dist[j] = dist[t] + g[t][j];
}
}
}
}

堆优化版:常用于稀疏图,点数的平方远大于边数

注意:在稠密图下,堆优化版有可能比朴素版要慢

原理:某个点的最短路,肯定是由某条从起点开始,到达上一个点的最短路更新而来

做法:利用堆去维护当前距离起点最近的点,每次取堆顶元素,标记该节点u为已确定点,利用该节点取更新所有出点v,如果能够满足$dist[v] > dis[u] + w$,则更新v节点的最短路,并将其放入堆中

代码实现(priority_queue版):

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
49
50
51
52
53
54
55
typedef pair<int, int> PII;

const int N = 2e5 + 10;

int n, m;
int d[N];
int e[N], w[N], h[N], ne[N], idx;
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(d, 0x3f, sizeof d);
// greater<PII>
priority_queue<PII, vector<PII>, greater<PII> > q;
d[1] = 0;
q.push({0, 1});

while (q.size()) {
PII t = q.top();
q.pop();

int u = t.second;
if (st[u]) continue;
st[u] = true;

for (int i = h[u]; i != -1; i = ne[i]) {
int j = e[i];
if (d[j] > d[u] + w[i]) {
d[j] = d[u] + w[i];
q.push({d[j], j});
}
}
}

if (d[n] == 0x3f3f3f3f) return -1;
else return d[n];
}

int main() {
cin >> n >> m;

memset(h, -1, sizeof h);
while (m -- ) {
int x, y, c;
scanf("%d%d%d", &x, &y, &c);
add(x, y, c);
}

cout << dijkstra() << endl;

return 0;
}

存在负权边:

Bellman-Ford

  • 可以求经过最多k条边的最短路
  • 也可以用于求带负权边的最短路和找负环,但由于复杂度过高,一般用SPFA

原理:迭代k次,每次遍历所有边(注意,不是点,和dijkstra不同),对于一条边,利用起点的dist值去更新终点的dist值

先介绍 Bellman–Ford 算法要用到的松弛操作(Dijkstra 算法也会用到松弛操作)。

对于边 $(u,v)$,松弛操作对应下面的式子:$dis(v) = \min(dis(v), dis(u) + w(u, v))$。

这么做的含义是显然的:我们尝试用 $S \to u \to v$(其中 $S \to u$ 的路径取最短路)这条路径去更新 $v$ 点最短路的长度,如果这条路径更优,就进行更新。

Bellman–Ford 算法所做的,就是不断尝试对图上每一条边进行松弛。我们每进行一轮循环,就对图上所有的边都尝试进行一次松弛操作,当一次循环中没有成功的松弛操作时,算法停止。

代码实现:

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
const int N = 510, M = 10010;

int n, m, k;
int d[N], last[N];
struct Edge{
int a, b, w;
}e[M];

int bellman_ford() {
memset(d, 0x3f, sizeof d);

d[1] = 0;
for (int i = 1; i <= k; i ++ ) {
// 拷贝数据,防止“串联”现象发生
memcpy(last, d, sizeof d);
for (int j = 1; j <= m; j ++ ) {
int a = e[j].a, b = e[j]. b, w = e[j]. w;
d[b] = min(d[b], last[a] + w);
}
}

// 由于有负权边的存在,这里不是0x3f3f3f3f
if (d[n] >= 0x3f3f3f3f / 2) return -1;
else return d[n];
}

int main() {
cin >> n >> m >> k;
for (int i = 1; i <= m; i ++ ) {
int x, y, w;
scanf("%d%d%d", &x, &y, &w);
e[i] = {x, y, w};
}

int t = bellman_ford();
if (t == -1) printf("impossible\n");
else printf("%d\n", t);

return 0;
}

SPFA

1.求带负权边的最短路

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
49
50
51
int n, m;
int d[N];
int e[N], h[N], w[N], ne[N], idx;
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(d, 0x3f, sizeof d);

queue<int> q;
d[1] = 0;
q.push(1);

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 (d[j] > d[t] + w[i]) {
d[j] = d[t] + w[i];
if (!st[j]) q.push(j);
}
}
}

if (d[n] == 0x3f3f3f3f) return -1;
else return d[n];
}

int main() {
cin >> n >> m;

memset(h, -1, sizeof h);
while (m -- ) {
int x, y, w;
scanf("%d%d%d", &x, &y, &w);
add(x, y, w);
}

int t = spfa();

if (t == -1) printf("impossible\n");
else printf("%d\n", t);

return 0;
}

2.找负环

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#include <bits/stdc++.h>
using namespace std;

typedef long long LL;
typedef unsigned long long ULL;
typedef pair<int, int> PII;

const int N = 1e5 + 10;

struct Edge {
int to, w;
};
vector<Edge> g[N];
int dist[N], cnt[N];
bool st[N]; // 记录是否在队列中

// 对于边权为正的图,任意两个结点之间的最短路,任意一条的结点数不会超过 $n$,边数不会超过 $n-1$。
bool spfa(int n) {
memset(dist, 0x3f, sizeof(dist));
memset(st, 0, sizeof(st));
memset(cnt, 0, sizeof(cnt));
dist[1] = 0;
queue<int> q;
q.push(1);
while (q.size()) {
int u = q.front();
q.pop();
st[u] = false;
for (auto e : g[u]) {
int v = e.to, w = e.w;
if (dist[v] > dist[u] + w) {
dist[v] = dist[u] + w;
cnt[v] = cnt[u] + 1;
// 根据负环性质,判定为存在负环,直接返回
if (cnt[v] >= n) return true;
if (!st[v]) {
q.push(v);
st[v] = true;
}
}
}
}

return false;
}

void solve() {
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i ++ ) g[i].clear();
while (m --) {
int a, b, c;
cin >> a >> b >> c;
if (c >= 0) {
g[a].push_back({b, c});
g[b].push_back({a, c});
} else {
g[a].push_back({b, c});
}
}

bool t = spfa(n);
if (t) cout << "YES\n";
else cout << "NO\n";
}

int main() {
std::ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
int T;
cin >> T;
while (T --) {
solve();
}
return 0;
}

多源汇最短路

Floyd算法

可以解决多源汇最短路,也可以解决传递闭包问题(相当于边权都为1的最短路)

思想:利用动态规划的思想,设$dist[i][j]$为从i出发到j的最短路,$dist[i][k]$为从i出发到k的最短路,$dist[k][j]$为从k出发到j的最短路,则$dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])$

做法:

初始化每个点到自己的最短路为0,其余最短路为正无穷

迭代n次,最外层枚举中间节点k,内两层分别枚举起点和终点,利用上述公式更新所有最短路

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
const int N = 210, INF = 1e9;

int n, m, k;
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;
// 初始化,当i和j相等表示自己走到自己,路径长度为0,其余均视作无穷大
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 = 1; i <= m; i ++ ) {
int x, y, z;
scanf("%d%d%d", &x, &y, &z);
d[x][y] = min(d[x][y], z); // 由于求最短路,只需要保留最短边即可
}

floyd();

// 处理输出结果
while (k -- ) {
int x, y;
scanf("%d%d", &x, &y);
if (d[x][y] >= INF / 2) printf("impossible\n");
else printf("%d\n", d[x][y]);
}

return 0;
}
传递闭包

从数学上来说,传递闭包是在集合$X$上求包含关系 $R$的最小传递关系。从关系图的角度来说,就是如果原关系图上有 $i$ 到 $j$ 的路径,则其传递闭包的关系图上就应有从 $i$ 到 $j$ 的。通俗地讲,就是确定每个点是否能到达其他每个点。

只需要将floyd算法中求最短路的部分替换为或运算即可

1
2
3
4
5
6
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] |= d[i][k] && d[k][j];
}

最短路算法
https://www.lry666.cn/posts/315/
作者
LRY666
发布于
2023年11月20日
许可协议