多源最短路径 - Floyd详解 - C语言

181 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第7天,点击查看活动详情


Floyd算法

Floyd算法是求解多源最短路径问题的典型算法,可以知道图中任意两点之间的最短路径。该算法对于有向图、无向图都适用,同时允许图中带有负权边,但是不允许有负权环。

Floyd算法采用动态规划的思想,分为多个阶段来解决问题。

若图GGnn个顶点(V0Vn1)(V_0 \sim V_{n-1}),则将求图中每一对顶点之间的最短路径分nn个阶段∶

Floyd求解过程:

  • 首先进行初始化,在没有其它顶点中转的情况下,求得各顶点间的最短路径;

  • 在各顶点间增加V0V_0作为中转结点,求得各顶点间新的最短路径;

  • 再增加V1V_1作为中转结点,求得各顶点间新的最短路径;

    ……

  • 最后增加Vn1V_{n-1}作为中转结点,求得各顶点间最终的最短路径。



3.1 算法思想

Floyd只能使用邻接矩阵来实现。

为了方便理解,我们来手动模拟一下实现过程。以有向图演示,无向图同理。

我们使用两个大小为n×nn \times n的二维数组分别记录最短路径disdis与中转顶点pathpath。其中,最短路径矩阵可以告诉我们任意两顶点间的最短距离;而中转顶点矩阵可以告诉我们路径。

(1)初始化

在这里插入图片描述 初始化的最短路径矩阵其实就是邻接矩阵,中转顶点矩阵全部标记为1-1,代表未经过中转。


(2)求解

① 加入V0V_0中转

可以看到,V0V_0顶点的入度为0,所以任何顶点都不能到达V0V_0,最短路径与中转顶点矩阵不变。

没有别的顶点可以通过V1V_1中转或使得disdis减少,进行下一次中转。

在这里插入图片描述

② 加入V1V_1中转

可以看到,当我们添加到V1V_1作为中转时,

原先V2V3=V_2 \rightarrow V_3 = \infty,现在V2V1V3=2V_2 \rightarrow V_1 \rightarrow V_3= 2,更新dis[V2][V3]=2dis[V_2][V_3]=2path[V2][V3]=1path[V_2][V_3]=1

原先V2V4=7V_2 \rightarrow V_4 = 7,现在V2V1V4=6V_2 \rightarrow V_1 \rightarrow V_4= 6,更新dis[V2][V4]=6dis[V_2][V_4]=6path[V2][V4]=1path[V_2][V_4]=1

没有别的顶点可以通过V1V_1中转或使得disdis减少,进行下一次中转。

在这里插入图片描述


③ 加入V2V_2中转

可以看到,当我们添加到V2V_2作为中转时,

原先dis[V0][V1]=dis[V_0][V_1] = \infty,现在dis[V0][V2]+dis[V2][V1]=2dis[V_0][V_2] + dis[V_2][V_1]= 2,更新dis[V0][V1]=2dis[V_0][V_1]=2path[V0][V1]=2path[V_0][V_1]=2

原先dis[V0][V3]=dis[V_0][V_3] = \infty,现在dis[V0][V2]+dis[V2][V3]=3dis[V_0][V_2] + dis[V_2][V_3]= 3,更新dis[V0][V3]=3dis[V_0][V_3]=3path[V0][V3]=2path[V_0][V_3]=2

原先dis[V0][V4]=10dis[V_0][V_4] = 10,现在dis[V0][V2]+dis[V2][V4]=7dis[V_0][V_2] + dis[V_2][V_4]= 7,更新dis[V0][V4]=7dis[V_0][V_4]=7path[V0][V4]=2path[V_0][V_4]=2

没有别的顶点可以通过V2V_2中转或使得disdis减少,进行下一次中转。

在这里插入图片描述


④ 加入V3V_3中转

可以看到,当我们添加到V3V_3作为中转时,

原先dis[V0][V4]=7dis[V_0][V_4] = 7,现在dis[V0][V3]+dis[V3][V4]=4dis[V_0][V_3] + dis[V_3][V_4]= 4,更新dis[V0][V4]=4dis[V_0][V_4]= 4path[V0][V4]=3path[V_0][V_4]=3

原先dis[V1][V4]=5dis[V_1][V_4] = 5,现在dis[V1][V3]+dis[V3][V4]=2dis[V_1][V_3] + dis[V_3][V_4]= 2,更新dis[V1][V4]=2dis[V_1][V_4]=2path[V1][V4]=3path[V_1][V_4]=3

原先dis[V2][V4]=6dis[V_2][V_4] = 6,现在dis[V2][V3]+dis[V3][V4]=3dis[V_2][V_3] + dis[V_3][V_4]= 3,更新dis[V2][V4]=3dis[V_2][V_4]=3path[V1][V4]=3path[V_1][V_4]=3

没有别的顶点可以通过V3V_3中转或使得disdis减少,进行下一次中转。

在这里插入图片描述


⑤ 加入V4V_4中转

可以看到,当我们添加到V4V_4作为中转时,由于V4V_4的出度为0,故不会进行更新,且已经将所有顶点中转完成,得到的便是最终结果。

在这里插入图片描述

(3)输出

dis[i][j]dis[i][j]存储的便是ViVjV_i \sim V_j的最短路径长度。

而想要输出ViVjV_i \sim V_j的最短路径,则需要顺着pathpath数组往前找。

以上图的V2V4V_2 \sim V_4顶点为例:

首先path[V2][V4]=3path[V_2][V_4]=3,则说明经过V3V_3进行中转,路径为V2(V3)V4V_2 \rightarrow (V_3) \rightarrow V_4

接着找path[V2][V3]=1path[V_2][V_3]=1,则说明经过V1V_1进行中转,路径为V2(V1)V3V4V_2 \rightarrow (V_1) \rightarrow V_3 \rightarrow V_4

接着找path[V2][V1]=1path[V_2][V_1]=-1,则说明没有经过任何顶点进行中转,得到最终的路径为V2V1V3V4V_2 \rightarrow V_1 \rightarrow V_3 \rightarrow V_4



3.2 实现

给出核心部分的C语言代码:

for (k = 0; k < VertexNum; k++) // 将每个顶点都作为中转尝试
{
    // 遍历整个矩阵 i-行 j-列
    for (i = 0; i < VertexNum; i++)
    {
        for (j = 0; j < VertexNum; j++)
        {
            // 若经过k顶点中转,路径更短,则更新矩阵
            if (dis[i][k] + dis[k][j] < dis[i][j])
            {
                dis[i][j] = dis[i][k] + dis[k][j]; // 更新dis矩阵
                path[i][j] = k;                    // 更新path矩阵
            }
        }
    }
}


3.3 算法分析

  • 空间复杂度O(V2)O(|V|^2)

  • 时间复杂度O(V3)O(|V|^3)

估计这时候你也看出了一些问题,我使用Dijkstra算法将每个顶点作为起点计算一遍,时间复杂度不也是O(V3)O(|V|^3)吗?那Floyd算法的优势在哪里呢?

其实,虽然两种算法都是O(V3)O(|V|^3),但是前面的系数并不相同,Floyd的系数会更小一些,所有效率也会更高一些。也因此,即使它的复杂度到达了O(V3)O(|V|^3)的程度,对于一些中小规模的图仍然是适用的。

同时Floyd也支持负权边,也是其一个优点。