我不会跟大家说我两个月前就写好了,只是今天才发出来。
本文概述
最短路算法,见名知意,就是用于求出图中从某个顶点到另一个顶点最短距离的算法。最短路算法的应用极其广泛。本文将会以求解最短路为中心,围绕着展开叙述一些常见的最短路算法的原理和应用。
根据最短路算法,我们大致地可以将算法分为两大类:
- 单源最短路径 Single Source Shortest Path:用于快速计算从某一个特定顶点出发到达任意一个点的距离。
- 多源最短路径 Multiple Source Shortest Path:用于快速计算任意两点之间的最短路径。
本文将会介绍的算法和各算法的特点如下:
- 深度优先搜索 Depth First Search:单源最短路径算法,可以求解从任意一点开始到另一点的最短路径,但该算法极其耗时。
- 广度优先搜索 Breadth First Search:单源最短路径算法,仅用于求解无权图。
- 迪克斯特拉算法 Dijkstra Algorithm:单源最短路径算法,是广度优先搜索算法的加强版。Dijkstra 算法不能处理带有负权边的情况,更不能处理带有负权回路的图。
- 贝尔曼福特算法 Bellman-Ford Algorithm:单源最短路径算法,可以用于处理又负权边的情况。对其进行队列优化后就变成了我们熟知的 SPFA 算法。
- 弗洛伊德算法 Floyd Algorithm:多源最短路径算法,基于动态规划思想,能够一次性求出图中任意两点之间的最短路径,但该算法的时间复杂度非常高,达到惊人的 O ( N 3 ) O(N^3) O(N3)。弗洛伊德算法能处理带有负权边的情况,但不能处理带有负权回路的图。
为了方便阅读,本文提供的所有代码均会使用 vector 实现的邻接表来存图。
场景引入
在下图中,有 7 7 7 条不同的边,每一条路径上方都标记了一个数值 T i T_i Ti,代表通过该条路径所需要的时间。Macw 想知道从 1 1 1 号顶点到 5 5 5 号顶点所需要花费的最短时间。

不难看出,Macw 所需要花费的最短时间是 14 14 14 分钟,一条可行的方案是从 1 1 1 号顶点出发,途径 2 2 2, 3 3 3 号顶点到达顶点 5 5 5。虽然人脑可以很快的看出来,但是在庞大的数据量下,人的脑力就显得极其渺小。那对于计算机来说,我们如何能找到一条从 1 1 1 号节点到 5 5 5 号节点的路径呢?不妨让计算机暴力枚举出所有的可行路径和每条路径分别的耗时,取最小的那一条路径就可以了。深度优先搜索算法是一个选择。
深度优先搜索 Depth First Search
如上图所示,从 1 1 1 号节点到 5 5 5 号节点共有四条路径,每条路径和其分别耗时如下:
- 1 → 2 → 5 1\to 2\to 5 1→2→5,耗时 2 + 16 = 18 2 + 16 = 18 2+16=18 分钟。
- 1 → 2 → 3 → 5 1\to 2\to 3\to 5 1→2→3→5,耗时 2 + 7 + 5 = 14 2 + 7 + 5 = 14 2+7+5=14 分钟。
- 1 → 4 → 5 1\to 4\to 5 1→4→5,耗时 6 + 12 = 18 6 + 12 = 18 6+12=18 分钟。
- 1 → 4 → 3 → 5 1 \to 4\to 3\to 5 1→4→3→5,耗时 6 + 6 + 5 = 17 6 + 6 + 5 = 17 6+6+5=17 分钟。
其中第二个方案是最优解,耗时 14 14 14 分钟。因此,一个可行的算法方案是使用深度优先搜索暴力枚举出所有可行的路径并记录最小值即可,其代码实现如下:
// ans 用于记录答案,一开始把答案设置为无穷大。
int ans = 0x7f7f7f7f;
// 记录一个点是否被访问过,防止出现循环。
int vis[105];
struct Edge{
int to;
int weight;
}; vector<Edge> G[105];
// 表示到 node 节点的耗时为 steps。
void dfs(int node, int steps){
if (steps >= ans) // 最优性剪枝。
// 如果走到目标答案,就更新结果并返回。
if (node == terminal){
ans = min(ans, steps);
return ;
}
for (Edge next : G[node]){
// 遍历从 node 开始所有的边。
if (vis[next.to]) continue ; // 被访问过了,就跳过。
vis[next.to] = 1; // 将节点标记为已访问的状态。
dfs(next.to, steps + next.weight); // 继续递归。
vis[next.to] = 0; // 将节点标记为未访问的状态。
}
return ;
}
深度优先嗖嗖算法虽然很有效,但是该算法的运行效率太低下了,只适用于数据量较小的情况。假设图是一个二叉树树形结构,那么在最坏的情况下,这个算法的时间复杂度将会达到 O ( 2 N ) O(2^N) O(2N)。当每个顶点的度越多,深度优先搜索算法的时间复杂度就高。因此,在一般情况下,我们不会使用深度优先搜索。
设想在二维 N × M N \times M N×M 地图问题的情况下,我们怎么找到从入口到出口的最佳路径?一般情况下,我们会使用广度优先搜索的算法。广度优先搜索算法的复杂度远远低于深度优先搜索。
广度优先搜索算法 Breadth First Search
在一个无权图中(或图中所有边的权值均为 1 1 1)的情况下,我们会使用广度优先搜索算法来实现。广度优先搜索算法的代码也很简洁:
struct node{
int pos;
int steps;
}; queue<node> que;
struct Edge{
int to;
int weight;
}; vector<Edge> G[105];
// 记录一个点是否被访问过,防止出现循环。
int vis[105];
void bfs(int start){
que.push(start) // 将起点加入队列。
while(!que.empty()){
node t = que.front();
que.pop();
// 如果走到目标答案,就输出答案并终止循环。
if (t.pos == terminal){
cout << t.steps << endl;
return ;
}
// 遍历从 t 开始所有的边。
for (Edge next : G[t]){
// 如果被访问过了,就略过。
if (vis[next.to]) continue;
vis[next.to] = 1;
// 将新的节点加入到队列之中。
que.push((node){
next.to, t.steps + next.weight});
}
}
return ;
}
我们已经知道,在一般的地图问题中广度优先搜索的效率非常高。那我们是否可以加以改进广度优先算法,让它适配带权图呢?答案是可以的,经过改编后的算法就是大名鼎鼎的 Dijkstra 算法。
迪克斯特拉算法 Dijkstra Algorithm
Dijkstra 算法是一种用于寻找图中从单一源节点到其他所有节点的最短路径的算法(即单源最短路径算法)。它适用于所有边权重为非负值的图。这个算法最早是被荷兰计算机科学家 艾兹赫尔·戴克斯特拉 (Edsger W. Dijkstra) 发明并提出的,因此用他的名字来命名该最短路算法。
Dijkstra 算法的基本思想是逐步扩展最短路径,直到覆盖所有节点。依旧以「场景引入」章节的图来举例子,虽然我们没有办法一下子立刻求解出从 1 1 1 号节点到 5 5 5 号节点的最短路径,但是如果我们能求出从 1 1 1 号节点到 5 5 5 号节点所有的前驱节点的最短路径,那么我们就可以立刻计算出从起点到 5 5 5 号节点最短路。

如上图所示, 5 5 5 号节点有三个前驱节点,分别是节点 V = { 2 , 3 , 4 } V = \{2, 3, 4\} V={ 2,3,4},走到这三个节点的最短距离分别为两分钟、九分钟和六分钟。通过遍历这些前驱节点,我们就可以求出从起点到终点的最短路。显然,对于图中所有的节点,我们都需要按照一定的顺序依次对它们进行相同的操作。这样子,我们就可以求解出从起点开始到图中任意一个点的最短距离了。
Dijkstra 算法具体的实现流程如下:
- 初始化:
- 创建一个数组,用于记录从源点开始到任意一点的距离。同时设定源节点的距离为 0 0 0,其他所有节点的距离为 + ∞ +\infty +∞。
- 将所有节点标记为未访问。
- 使用一个优先队列来存储节点及到某一个节点当前所计算出的最短距离。
- 选择节点:
- 从未访问的节点中选择当前距离最小的节点作为当前节点。
- PS:如没有特殊情况,一开始这个节点应该是求解最短路问题的起点(起点与自己的距离应该是 0 0 0,正如初始化步骤中所提及的)。
- 更新节点:
- 对于当前节点的每个邻居节点,计算从当前节点到该邻居节点的距离。
- 如果这个距离小于已知的到该邻居节点的距离,则更新该邻居节点的距离。
- 距离更新: 对于每个邻居节点,计算从起点到该邻居节点的距离,如果该距离小于已知的最短距离,则更新最短距离。
- 标记已访问:
- 将当前节点标记为已访问,表示已经处理完了该节点。
- PS:当节点被标记完已访问后,从起点到该节点的最短距离就已经被正式确定下来了,在后续的计算过程中该节点的距离将不会再被更新。标记节点已访问可以在「选择节点」步骤完成后时就进行。换句话说,第三步和第四步的顺序并不重要。
- 重复循环:
- 重复上述提到的第 2-4 步,直到所有的点都被标记为已访问。
以下是使用 Dijkstra 算法对例题的模拟过程:
首先初始化距离数组,将源点的距离设置为 0 0 0,将除源点以外的所有点的距离设置为 + ∞ +\infty +∞。正无穷大表示到达该点的最短距离还未知。

从未访问的节点中选择当前距离最小的节点作为当前节点。在一开始,距离最小的节点就是源点本身。如下图,浅绿色表示当前选中的节点,黄色表示该节点的邻居节点。接下来就开始更新两个邻居节点距源点的最近距离。从 1 1 1 号点到 2 2 2 号点的最短距离为 2 2 2,而 2 2 2 号节点当前所记录的最短距离是 + ∞ +\infty +∞,比较发现 2 < + ∞ 2 < +\infty 2<+∞,因此将 2 2 2 号点的距离从原本的正无穷更新为 2 2 2。节点 4 4 4 也是如此,从起点到该节点的最短路径将由原本的正无穷更新为 6 6 6。

此时, 1 1 1 号节点已经处理完成了,我们将该节点标记为已访问(图中用深绿色表示)。接下来,我们从未访问的节点当中选择一个距离最小的节点作为当前节点。如下图,未访问的节点有


3714

被折叠的 条评论
为什么被折叠?



