[知识点] 8.5 最短路

总目录 > 8 图论 > 8.5 最短路

前言

图论基础算法中最经典的问题!因为实在是太常见了,适用于不同情况的算法也很多。

当时对知识框架并没有概念的时候,对最短路的算法还是挺记忆犹新的,Floyd 和 Dijkstra 的双排故事还在继续。

子目录列表

1、最短路径

2、Floyd 算法

3、Dijkstra 算法

4、Bellman-Foed算法(施工中)

5、SPFA 算法(施工中)

6、Johnson 算法(施工中)

8.5 最短路

1、最短路径

家住信阳的小 k 和小 q 想去贵阳玩耍,因为奇妙的原因,他们只能步行过去,现在手里有一张地图,只能沿着地图上的路径走,不考虑其他因素,他们想知道怎么走能最快到达贵阳?

当然这是一张简化了许多的地图。通过手算,我们发现 “信阳 -> 武汉 -> 岳阳 -> 长沙 -> 邵阳 -> 贵阳” 这条路是最短的,距离为 19,也就是时间花费最少的。

如果地图变得庞大,我们就要采用各种手段来求了!

2、Floyd 算法

① 介绍

Robert Floyd 老先生于 1962 年提出的这个算法。将所有结点的任意两个结点之间的最短路同时求出,是一种多源最短路径算法。适用于需要求出多对起点与终点的情况。因为状态数组初始化直接保存边权,所以不需要单独存储图了。

② 核心思想

初始默认 n 个结点只能通过直接相连的边到达,即假设结点 x 和 结点 y 之间有一条边权为 w 的边,则它们的最短路为 w;如果没有边,则它们不可达。显然当前不一定是最短路径,接下来每次对其中一个结点 i 进行分析,对于其他任意两个结点 x 和 y,判断是否存在 x - i - y (x < i < y) 这样的路径,如果存在,比较其路径长度与当前记录的 x 和 y 之间的最短路长度,如果更优,则更新最短路长度。

③ 举例

暂略。

④ 实现

Floyd 算法本质上是一种动态规划(请参见:4.1 动态规划基础 / 记忆化搜索)。所以,我们用动态规划的框架来分析:

定义一个数组 f[i][j][k],表示只允许经过编号在 [1, i] 范围内的结点时,结点 j 到结点 k 的最短路长度。显然,f[n][j][k] 即结点 j 到结点 k 的最短路长度。

初始化,结点 j 到结点 k 如果没有相连的边,f[0][j][k] = 0;有,则 f[0][j][k] = 边权;j = k 时,f[0][j][k] = +∞。

根据思想,如果现在分析的结点 i 对结点 j 和 k 之间的最短路长度没有影响,则 f[i][j][k] = f[i - 1][j][k];如果有更优解,则更新,即 f[i][j][k] = f[i - 1][j][i] + f[i - 1][i][k])。不难发现,其实数组中的第一维是不必要的,递推过程是单调的,则可以直接略去。

综上可得:

状态数组:f[i][j] 表示 “结点 i 到结点 j 的最短路长度”;

状态转移:f[i][j] = min(f[i][j], f[i][k] + f[k][j]), k ∈ (i, j)。

⑤ 核心代码

for (int k = 1; k <= n; k++)
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++)
            f[i][j] = min(f[i][j], f[i][k] + f[k][j]);

⑥ 优点

> 适用于任何图,有向无向,正权负权,只要最短路存在(即不存在负环),均可以使用;

> 很好理解,实现相当简单,三个 for 循环即可。

⑦ 缺点

> 复杂度极差。总共对 n 个结点进行分析,每次 x 和 y 的选择种数为 n ^ 2 个,故时间复杂度高达为 O(n ^ 3)。而空间复杂度也不甘示弱 O(n ^ 2)。大多数情况下是不满足题目需求的。

3、Dijkstra 算法

① 介绍

Edsger Wybe Dijkstra 老先生于 1959 年提出的这个算法,属于单源最短路径算法。主要特点是以源点为中心向外层层扩展,直到扩展到终点为止,是最具代表性的最短路算法之一。

Dijkstra 算法要求图中不存在负权边,不然不能求得正确答案。

② 核心思想

将图中结点集合分成两组,一组为已求出最短路长度的结点集合,用集合 S 表示,另一组为未求出的,用集合 U 表示。将源点放入集合 S,其到源点的最短路长度显然为 0。每次从集合 U 中按照最短路长度从小到大取出一个结点,更新其他相邻结点的最短路长度,并将其加入集合 S,直到所有结点均在集合 S 中,即处理完毕。

③ 举例

暂略。

④ 实现

定义一个数组 dis[i],表示从源点到结点 i 的当前最短路长度。先进行初始化,i 为源点时,dis[i] = 0;i 不是源点时,将其初始化为 +∞,表示默认不可达。定义一个数组 vis[i],表示结点 i 是否在集合 S 中

从集合 U 中取出当前 dis 值最小的结点 o(第一次取显然为源点本身),对于所有与结点 o 相邻的结点 v,判断从源点经过结点 o 最终到该点的路径长度是否比原先记录的当前最短路长度更小,即判断 dis[v] > dis[o] + w,w 表示结点 o 和结点 v 之间边的边权。是否满足如果是,则更新该长度。

重复上述步骤,直到集合 U 中没有元素,表示所有结点均已求出最短路长度。

⑤ 核心代码

 1 memset(dis, INF, sizeof(dis));
 2 dis[s] = 0;
 3 
 4 for (int i = 1; i <= n; i++) {
 5     int mi = INF, o;
 6     for (int j = 1; j <= n; j++)
 7         if (!vis[j] && dis[j] < mi)
 8             mi = dis[j], o = j;
 9     vis[o] = 1;
10     for (int x = h[o]; x; x = e[x].nxt) {
11         int v = e[x].v;
12         if (!vis[v] && dis[o] + e[x].w < dis[v])
13             dis[v] = dis[o] + e[x].w;
14     }
15 }

⑥ 堆优化 Dijkstra 算法

上面介绍的是最基础的 Dijkstra 算法,不难看出其时间复杂度为 O(n ^ 2),而其实它的优化方式很多,优化程度也各异。下面主要介绍堆优化。

> 堆优化

> 优先队列优化

> 线段树优化

> 斐波那契堆优化

⑦ 优点

> 时间复杂度优秀,尤其在使用了各类优化后,能适用于许多数据量大的情况;

> 适用面广,难以被卡数据。

⑧ 缺点

> 无法处理边权出现负数的情况;

> 如果需要把所有点先后作为源点来求解的话,效率不明显优于 Floyd 算法。

4、Bellman-Ford算法(施工中)

5、SPFA 算法(施工中)

6、Johnson 算法(施工中)

原文地址:https://www.cnblogs.com/jinkun113/p/13063162.html