3.1 最短路问题

地图

{{{Dijkstra:(mn2)O(n2)Dijkstra:(mn)O(mlogn){BellmanFord:O(nm)SPFA::O(m),O(nm)    Floyd:O(n3)最短路问题\begin{cases} 单源最短路 \begin{cases} 所有边权为正 \begin{cases} 朴素Dijkstra算法:稠密图(m \sim n^2) - O(n^2)\\ \\ 堆优化版Dijkstra算法:稀疏图(m \sim n) - O(mlogn)\\ \end{cases} \\ \\ 存在负权边 \begin{cases} Bellman-Ford算法:O(nm)\\ \\ SPFA算法:平均:O(m),最坏O(nm)\\ \end{cases} \\ \end{cases} \\ \\ \\ 多源汇最短路 \ \ \ \ Floyd算法:O(n^3)\\ \\ \\ \end{cases}

注:$ n $ 为点数,$ m $ 为边数

3.1.1 朴素Dijkstra算法

思想

(0) $ dist[i] $ 为 $ 1 $ 号点到 $ i $ 号点的当前最短距离
    集合SS存储当前已经确定有最短距离的点
(1) 初始化距离:$ dist[1] = 0, dist[i] = +\infty $
(2) $ for \ i : 0 \to n $
i.$\ t \ \leftarrow \ $ { 不在 SS 中的,距离11号点最近的点 }
ii.$\ S \ \leftarrow t \ $
iii. 用 $ t $ 更新其他点的距离

FAQ

(1) 存储方式:邻接矩阵

设邻接矩阵为$ g[N][N] $ $$ g[a][b] = \begin{cases} 0 \ , 存在从a号点到b号点的有向边\\ 1 \ , 不存在从a号点到b号点的有向边\\ \end{cases} $$

(2) 初始化方式:

1
memset(a, 0x3f, sizeof a)

memset函数是以字节为单位进行赋值的

void *memset(void *s, int ch, size_t n);
将 $ s $ 所指向的某一块内存中的前 $ n $ 个字节的内容全部设置为 $ ch $ 指定的 $ ASCII $ 值
$ 0x3f = 0011 1111 = 63 $
C++中 $ int $ 型变量所占的位数为 $ 4 $ 个字节,即 $ 32 $ 位
$ 0x3f $ 显然不是 $ int $ 型变量中单个字节的最大值,应该是 $ 0x7f = 0111 1111 B $
那为什么要赋值 $ 0x3f $ :
a. 作为无穷大使用,因为 $ 4 $ 个字节均为 $ 0x3f $ 时,$ 0x3f3f3f3f $ 的十进制是 $ 1061109567 $ ,也就是 $ 10^ 9 $ 级别的(和 $ 0x7fffffff $ 一个数量级)
b.  $ 0x3f3f3f3f + 0x3f3f3f3f = 2122219134 $ ,这非常大但却没有超过 $ 32-bit \ int $ 的表示范围,满足了“无穷大加无穷大还是无穷大”的需求。

(3) 如何处理重边和自环:任意两点之间的有向边取边权最小

代码

nm1n1n1nmmx,y,zxyz 给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为正值。\\ 请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 −1。\\ 第一行包含整数 n 和 m \\ 接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。\\

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
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 510;

int n, m;
int g[N][N]; // 邻接矩阵
int dist[N]; // 从1号点走到每个点的当前的最短距离
bool st[N]; // 每个点的最短路是否已经被确定,作用是充当集合S

int dijkstra()
{
// 初始化距离
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
for(int i = 0; i < n; i ++)
{
int t = -1; // t 表示不在S中的,当前距离最短的点,初始化为一个不合法的点
for (int j = 1; j <= n; j ++)
{
// 找t
if (!st[j] && (t == -1 || dist[t] > dist[j]))
{
// 如果t不在S中
// 如果t还没有成为一个合法的点 或者 j点的距离比t点的短,就用j更新t
t = j;
}
}
st[t] = true; // t放入集合S中
for (int j = 1; j <= n; j ++)
{
// 用t更新距离
dist[j] = min(dist[j], dist[t] + g[t][j]);
}
}

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

int main() {
scanf("%d%d", &n, &m);

memset(g, 0x3f, sizeof g); // 初始化邻接矩阵

while (m --) {
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
g[a][b] = min(g[a][b], c); // 处理重边和自环
}

int t = dijkstra();

printf("%d\n", t);
return 0;
}

3.1.2 堆优化Dijkstra算法

代码

nm1n1n1nmmx,y,zxyz1n11n,m1.5×105010000109 给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为非负值。\\ 请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 −1。\\ 输入格式:第一行包含整数 n 和 m。\\ 接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。\\ 输出格式:输出一个整数,表示 1 号点到 n 号点的最短距离。如果路径不存在,则输出 −1。\\ 数据范围:1≤n,m≤1.5×10^5 \\ 图中涉及边长均不小于 0,且不超过 10000。数据保证:如果最短路存在,则最短路的长度不超过 109。\\
TODO:C++优先队列,邻接表,堆

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
#include <iostream>
#include <queue> // 优先队列的库
#include <cstring> // memset的库

using namespace std;

const int N = 1e6;
int n, m; // n为点数, m为边数
int h[N], e[N], w[N], ne[N], idx; // 邻接表
int dist[N]; // 距离
bool st[N]; // 状态
typedef pair<int, int> PII; // <距离,编号>

void add(int a, int b, int c) {
/*
头插法
h[i]:i号点所指向的某一个节点的下标
通过idx来索引
e[i]:节点的值,即点的序号
w[i]:有向边的权重
ne[i]:e[i]所指向的某一个节点的下标
*/
// a --c--> b
e[idx] = b;
w[idx] = c;
ne[idx] = h[a];
h[a] = idx;
idx ++;
}

int dijkstra() {
memset(dist, 0x3f, sizeof dist); // 初始化距离
dist[1] = 0; // 初始化距离
priority_queue<PII, vector<PII>, greater<PII>> heap; // [STL]小根堆,堆用vector实现
heap.push({0, 1}); // 1号点入堆
while (heap.size()) {
auto t = heap.top(); // 找出距离最近的点
heap.pop(); // 出堆
int ver = t.second; // 点的编号
int d = t.first; // 距离
if (st[ver]) continue;
st[ver] = true;
// 更新,遍历从t出去的所有边
for(int i = h[ver]; i != -1; 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); // 初始化邻接表,h[i] = -1
while (m --) {
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c);
}
cout << dijkstra() << endl;
return 0;
}

3.1.3 Bellman-Ford算法

代码

nm1nk1nimpossiblen,m,kmx,y,zxyz1n1nkimpossible1n,k500,1m10000,1x,yn10000 给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。\\ 请你求出从 1 号点到 n 号点的最多经过 k 条边的最短距离,如果无法从 1 号点走到 n 号点,输出 impossible。\\ 注意:图中可能 存在负权回路 。\\ 输入格式:第一行包含三个整数 n,m,k。\\ 接下来 m 行,每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。点的编号为 1∼n。\\ 输出格式:输出一个整数,表示从 1 号点到 n 号点的最多经过 k 条边的最短距离。\\ 如果不存在满足条件的路径,则输出 impossible。\\ 数据范围1≤n,k≤500,1≤m≤10000,1≤x,y≤n,任意边长的绝对值不超过 10000。\\

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
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 510, M = 100010;
int n, m, k;
int dist[N], backup[N];
struct Edge {
// 最简单的结构体存储有向边
// a --w--> b
int a, b, w;
}edges[M];

int bellman_ford() {
memset(dist, 0x3f, sizeof dist); // 初始化
dist[1] = 0; // 初始化
for (int i = 0; i < k ; i ++) {
// 循环k次求出的最短路最多经过k条边
// 备份,防止在更新的时候使用更新后的数据
memcpy(backup, dist, sizeof dist);
for (int j = 0; j < m; j ++) {
int a = edges[j].a, b = edges[j].b, w = edges[j].w;
if (dist[b] > backup[a] + w) dist[b] = backup[a] + w;
}
}
// 距离为正无穷的点可能被负权边更新
if (dist[n] > 0x3f3f3f3f / 2) return -0x3f3f3f3f;
return dist[n];
}

int main() {
scanf("%d%d%d", &n, &m, &k);
for (int i = 0; i < m; i ++) {
int a, b, w;
scanf("%d%d%d", &a, &b, &w);
edges[i] = {a, b, w};
}
int t = bellman_ford();
if (t == -0x3f3f3f3f) puts("impossible");
else printf("%d\n", t);
return 0;
}

3.1.4 SPFA算法

代码

3.1.4.1 求最短路

nm1n1nimpossiblenmmx,y,zxyz1nimpossible1n,m105,10000 给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。\\ 请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 impossible。数据保证不存在负权回路。\\ 输入格式:第一行包含整数 n 和 m。\\ 接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。\\ 输出格式:输出一个整数,表示 1 号点到 n 号点的最短距离。如果路径不存在,则输出 impossible。\\ 数据范围:1≤n,m≤105,图中涉及边长绝对值均不超过 10000。\\

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
#include <cstring>
#include <iostream>
#include <queue>

using namespace std;

const int N = 100010, M = 100010;
int n, m;
int h[N], e[N], w[N], ne[N], idx;
int dist[N];
bool st[N];

void add(int x, int y, int z) {
e[idx] = y;
w[idx] = z;
ne[idx] = h[x];
h[x] = idx ++;
}

int spfa() {
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
// 优化Bellman-Ford算法中的松弛操作
// 只更新距离变小的点
// 方法:加入队列,更新它的所有出边,踢出队列
// 存储方式:邻接表
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;
}

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

int main() {
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
while (m --) {
int x, y, z;
scanf("%d%d%d", &x, &y, &z);
add(x, y, z);
}
int t = spfa();
if (t == -0x3f3f3f3f) puts("impossible");
else printf("%d\n", t);
}

3.1.4.2 判断负环

nmnmmx,y,zxyzYesNo1n2000,1m10000,10000 给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。判断图中是否存在负权回路。\\ 输入格式:第一行包含整数 n 和 m。\\ 接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。\\ 输出格式:如果图中存在负权回路,则输出 Yes,否则输出 No。\\ 数据范围:1≤n≤2000,1≤m≤10000,图中涉及边长绝对值均不超过 10000。\\

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
#include <cstring>
#include <iostream>
#include <queue>

using namespace std;

const int N = 100010, M = 100010;
int n, m;
int h[N], e[N], w[N], ne[N], idx;
int dist[N], cnt[N]; // cnt[i]表示i号点最短路的边数
bool st[N];

void add(int x, int y, int z) {
e[idx] = y;
w[idx] = z;
ne[idx] = h[x];
h[x] = idx ++;
}

bool spfa() {
// 不用求dist[n],不用初始化
// memset(dist, 0x3f, sizeof dist);
// dist[1] = 0;
// 优化Bellman-Ford算法中的松弛操作
// 只更新距离变小的点
// 方法:加入队列,更新它的所有出边,踢出队列
// 存储方式:邻接表
queue<int> q;
for (int i = 1; i <= n; i ++) {
// 可能有1号点到不了的负环,每个点都要当作起点
q.push(i);
st[i] = 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];
cnt[j] = cnt[t] + 1; // 更新的话边数+1
if(cnt[j] >= n) return true;
if (!st[j]) {
q.push(j);
st[j] = true;
}

}
}
}
return false;
}

int main() {
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
while (m --) {
int x, y, z;
scanf("%d%d%d", &x, &y, &z);
add(x, y, z);
}
if (spfa()) puts("Yes");
else puts("No");
return 0;
}

3.1.5 Floyd算法

代码

nmkxyxyimpossiblen,m,kmx,y,zxyzkx,yxykimpossible;1n200,1kn2,1m20000,10000 给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,边权可能为负数。\\ 再给定 k 个询问,每个询问包含两个整数 x 和 y,表示查询从点 x 到点 y 的最短距离。\\ 如果路径不存在,则输出 impossible。数据保证图中不存在负权回路。\\ 输入格式:第一行包含三个整数 n,m,k。\\ 接下来 m 行,每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。\\ 接下来 k 行,每行包含两个整数 x,y,表示询问点 x 到点 y 的最短距离。\\ 输出格式:共 k 行,每行输出一个整数,表示询问的结果,若询问两点间不存在路径,则输出 impossible。\\ 数据范围;1≤n≤200,1≤k≤n^2,1≤m≤20000,图中涉及边长绝对值均不超过 10000。\\

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
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 210, INF = 1e9;
int d[N][N];
int n, m, Q;

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);
if (d[a][b] > INF / 2) puts("impossible");
else printf("%d\n", d[a][b]);
}
}