最短路

前言

前置知识

正文

单源最短路

单源最短路径问题 (Single Source Shortest Path, SSSP 问题) 是说,给你一张有向图 G = (V, E),V 是点集,E 是边集,|V| = n, |E| = m,节点以 [1, n]之间的连续整数编号,(x, y, z) 描述一条从 x 出发, 到达 y,长度 / 边权为 z 的有向边。设 1 号点为起点,求长度为 n 的数组 dist,其中 dist[i] 表示从起点 1 到 节点 i 的最短路径的长度。

Dijkstra 算法

Dijkstra 算法的流程如下:

  1. 初始化 dis[1] = 0,其余节点的 dis 值均为正无穷大。
  2. 找出一个未被标记的、 dis[x] 最小的节点 x,然后标记节点 x。
  3. 扫描节点 x 的所有出边 (x, y, z),若 dis[y] > dis[x] + z,则使用 dis[x] + z 更新 dis[y]。
  4. 重复上述 2~3 两个步骤,直到所有节点都被标记。

Dijkstra 算法采用贪心策略,仅用于解决带非负权图的单源最短路问题。
当边长 z 都是非负数时,全局最小值不可能再被其他节点更新,故在第 1 步中选出的节点 x 必然满足:dis[x] 已经是起点到 x 的最短路径。

实现

以下是朴素算法的实现示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct edge {
int v, w;
};

vector<edge> e[MAXN];
int dis[MAXN], vis[MAXN];

void dijkstra(int n, int s) {
memset(dis, 0x3f, (n + 1) * sizeof(int));
dis[s] = 0;
for (int i = 1; i <= n; i++) {
int u = 0, mind = 0x3f3f3f3f;
for (int j = 1; j <= n; j++)
if(!vis[j] && dis[j] < mind) u = j, mind = dis[j]; //找到所有未标记的点中 dis 最小的
vis[u] = true;
for (auto ed : e[u]) {
int v = ed.v, w = ed.w;
if(dis[v] > dis[u] + w) dis[v] = dis[u] + w;
}
}
}

上面程序的时间复杂度为 O(n2)
如果我们使用优先队列来维护 dis 数组,则可在 O(mlog 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
26
27
28
29
30
31
32
struct edge {
int v, w;
};

struct node {
int dis, u;
bool operator>(const node& a) const { return dis > a.dis; }
};

vector<edge> e[MAXN];
int dis[MAXN], vis[MAXN];
priority_queue<node, vector<node>, greater<node>> q;

void dijkstra(int n, int s) {
memset(dis, 0x3f, (n + 1) * sizeof(int));
memset(vis, 0, (n + 1) * sizeof(int));
dis[s] = 0;
q.push({0, s});
while (!q.empty()) {
int u = q.top().u;
q.pop();
if (vis[u]) continue;
vis[u] = 1;
for (auto ed : e[u]) {
int v = ed.v, w = ed.w;
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
q.push({dis[v], v});
}
}
}
}

Bellman–Ford 算法

Bellman–Ford 算法是一种基于松弛 (relax) 操作的最短路算法,可以求出有负权的图的最短路,并可以对最短路不存在的情况进行判断。

对于一条边 (u, v),有以下松弛操作:dis(v) = min(dis(v), dis(u) + w(u, v));
Bellman-Ford 算法所做的,就是不断尝试对图上每一条边进行松弛。
即遵循以下流程:

  1. 扫描所有边 (u, v, w), 若 dis(v) > dis (u) + w(u, v),则用 dis (u) + w(u, v) 的值更新 dis(u)。
  2. 重复上述步骤,直到无法更新。

在最短路存在的情况下,由于一次松弛操作会使最短路的边数至少 + 1,而最短路的边数最多为 n - 1,因此整个算法最多执行 n - 1 轮松弛操作。故总时间复杂度为 O(nm)。

实现

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
struct Edge {
int u, v, w;
};

vector<Edge> edge;

int dis[MAXN], u, v, w;
constexpr int INF = 0x3f3f3f3f;

bool bellmanford(int n, int s) {
memset(dis, 0x3f, (n + 1) * sizeof(int));
dis[s] = 0;
bool flag = false;
for (int i = 1; i <= n; i++) {
flag = false; // 判断一轮循环过程中是否发生松弛操作
for (int j = 0; j < edge.size(); j++) {
u = edge[j].u, v = edge[j].v, w = edge[j].w;
if (dis[u] == INF) continue;
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
flag = true;
}
}
// 没有可以松弛的边时就停止算法
if (!flag) {
break;
}
}
// 第 n 轮循环仍然可以松弛时说明 s 点可以抵达一个负环
return flag;
}

SPFA 算法

SPFA 算法即是“队列优化的 Bellman-Ford 算法”。
流程如下:

  1. 建立一个队列,最初队列仅包含起点
  2. 取出队头节点 u,扫描它的所有出边 (u, v, w),若 dis(v) > dis (u) + w(u, v),则用 dis (u) + w(u, v) 的值更新 dis(u)。同时,若 v 不在队列中,则把 v 入队。
  3. 重复上述步骤,直到队列为空。

以下是 SPFA 算法的实现:

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
struct edge {
int v, w;
};

vector<edge> e[MAXN];
int dis[MAXN], cnt[MAXN], vis[MAXN];
queue<int> q;

bool spfa(int n, int s) {
memset(dis, 0x3f, (n + 1) * sizeof(int));
dis[s] = 0, vis[s] = 1;
q.push(s);
while (!q.empty()) {
int u = q.front();
q.pop(), vis[u] = 0;
for (auto ed : e[u]) {
int v = ed.v, w = ed.w;
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
cnt[v] = cnt[u] + 1; // 记录最短路经过的边数
if (cnt[v] >= n) return false;
// 在不经过负环的情况下,最短路至多经过 n - 1 条边
// 因此如果经过了多于 n 条边,一定说明经过了负环
if (!vis[v]) q.push(v), vis[v] = 1;
}
}
}
return true;
}

任意两点间最短路径

Floyd 算法

适用任何图
我们令 f[x][y] 表示 x 到 y 的最短路径,满足任意一个点 k,有 f[x][y] = min(f[x][y], f[x][k] + f[k][y])。
因为我们引进了点 k,所以该算法的复杂度为 O(n3)。

以下是 Floyd 算法的实现方式:

1
2
3
4
5
6
7
for (k = 1; k <= n; k++) {
for (x = 1; x <= n; x++) {
for (y = 1; y <= n; y++) {
f[x][y] = min(f[x][y], f[x][k] + f[k][y]);
}
}
}

最短路
http://example.com/2025/03/28/最短路/
作者
Soapoison
发布于
2025年3月28日
许可协议