Floyd Warshall 算法的时间复杂度

Posted

技术标签:

【中文标题】Floyd Warshall 算法的时间复杂度【英文标题】:Time complexity of Floyd Warshall algorithm 【发布时间】:2012-06-02 11:46:23 【问题描述】:

Skiena 的算法书包含对Floyd Warshall algorithm 的以下解释:

 floyd(adjacency_matrix *g)
 
   int i,j; /* dimension counters */
   int k; /* intermediate vertex counter */
   int through_k; /* distance through vertex k */
   for (k=1; k<=g->nvertices; k++)
       for (i=1; i<=g->nvertices; i++)
           for (j=1; j<=g->nvertices; j++) 
                through_k = g->weight[i][k]+g->weight[k][j];
                if (through_k < g->weight[i][j])
                      g->weight[i][j] = through_k;
           
  

Floyd-Warshall 全对最短路径在 O(n3) 时间内运行,这是渐近的 不比对 Dijkstra 算法的 n 次调用更好。然而,循环是如此 紧凑且程序如此之短,以至于它在实践中运行得更好。它是值得注意的 在邻接矩阵上比邻接更有效的稀有图算法 列表。

有人可以详细说明为什么粗体部分是真的吗?

【问题讨论】:

您是否尝试过编写代码来运行 n 个 Dijkstra 算法实例?... 【参考方案1】:

让我们分解一下:

Floyd-Warshall 全对最短路径在 O(n3) 时间内运行 ...

这是因为我们有一个三重 for 循环,每个循环都有 n 次迭代

... 这在渐近上并不比对 Dijkstra 算法的 n 次调用更好。 ...

回想一下,对 Dijkstra 算法的一次调用将告诉我们从一个特定节点 x1 到图中所有节点的所有最短路径。因此,我们可以对图中的所有节点进行 n 次 Dijkstra 算法的调用:x1x2、... xn 找到从 x1 到所有节点、x2 到所有节点、... xn 到所有节点的最短路径。换句话说,这为我们提供了所有对最短路径——就像 Floyd Warshall 所做的那样!

Running Dijkstra n times:
time = number of nodes × O((n + e)lgn)      [assuming binary heap used for Dijkstra]
     = n × O((n + e)lgn)                
     = O(n(n + e)lgn)

因此,Floyd-Warshall 的 O(n^3) 时间确实不比 O(n(n+e)lgn) 好 strong> 向 Dijkstra 发出 n 个调用的时间。

...但是,循环太紧,程序太短以至于在实践中运行得更好。

这里的关键词是“in practice”。请记住,渐近分析并不完美。这是性能的数学抽象/近似。当我们实际来运行代码时,有很多实际因素没有考虑到。处理器具有用于获取和运行指令的复杂低级架构。它pipelines 指令,pre-fetches 指令,尝试predict 指令,caches 指令和数据,......这是非常不可预测的!然而,所有这些低级优化都会对算法的整体性能产生巨大影响。理论上慢的算法可以得到提升,而理论上快的算法可能不会得到同样的提升。这有时被称为大哦符号的hidden constant factor。

事实证明,处理器喜欢优化嵌套的for 循环和多维数组!这就是 Skiena 在这里所暗示的。 for 循环阵列充分利用了temporal and spacial locality 并与低级处理器优化配合良好。另一方面,Dijkstra 的算法不能很好地做到这一点,因此处理器优化也不能很好地工作。因此,Dijkstra 在实践中确实可能会更慢。 Floyd-Warshall 是一个“短程序”,它不使用任何复杂的数据结构,并且重复的指令数量很少。这些东西,连同处理器优化一起,导致 Floyd-Warshall 有一个小的隐藏常数因子。也就是说,O(k·n3)中的k很小。

【讨论】:

【参考方案2】:

假设 v 为顶点数。对于稀疏图(少数边),边数e = O(v)。对于密集图(许多边)e = O(v^2)

现在最短路径问题的最佳渐近实现来自单一来源需要O(e + vlogv)摊销时间。 Dijkstra 算法的这种实现使用了斐波那契堆,由于涉及的常量值很高,因此不太实用。 例如,除了父节点和子节点之外,堆中的每个顶点也使用双链表连接到其兄弟节点。这导致每个节点存储大量指针。 除了堆,每个顶点都需要访问邻接表。

如果我们假设我们的图是密集图的最坏情况,e = O(v^2),Dijkstra 的算法将采用 O(v^2 + vlogv) = O(v^2)。 现在您将如何找到所有顶点对之间的最短路径

选项 1:

您可以使用 Dijkstra 算法并将其应用于每个顶点

那要花多少钱? v * O(v^2) = O(v^3)。 但是,所涉及的常数会使实际成本更高。您必须(一次)构建堆,检查邻接列表,减少键并提取每个顶点的最小值(同时保持最小堆)。

选项 2:

Floyd-Warshall 算法基本上适用于 v * v 邻接矩阵。它会考虑每个顶点并决定如果您可以通过该顶点,更短的路线会是什么。这是对矩阵的所有 v^2 元素执行的恒定时间比较和插入操作(到 2D 数组中)。

这需要对每个顶点执行。因此,时间复杂度为 O(v^3),但具有非常小的常数值,使其在实施过程中非常可行。

因此,您只需要一个邻接矩阵格式的图形、一个用于存储新值的邻接矩阵和 3 个嵌套的 for 循环,总共运行 v * v * v 次。我猜这就是紧凑和简单的意思!

【讨论】:

【参考方案3】:

但是,循环太紧,程序太短以至于可以运行 在实践中更好。

如果您查看该算法,就会发现有 3 个循环,其中只有 2 个语句嵌套在其中。唯一的逻辑是在第三个嵌套循环中完成的。如果您运行 n Djikstra's,逻辑将在第一个循环以及最后一个嵌套中完成,这很容易争论不干净。使用干净的三个循环,计算机也应该可以更轻松地管理内存。

【讨论】:

【参考方案4】:

Dijkstra 的通用数据结构算法是 O(E log v),但斐波那契堆将其改进为 O(E+v log v),这是更复杂的数据结构,算法的常量工厂比 floyed 更大。 看看这个link的运行时间。

这个问题的区别与q-sort和heap-sort之间的区别一样

【讨论】:

以上是关于Floyd Warshall 算法的时间复杂度的主要内容,如果未能解决你的问题,请参考以下文章

适用于负循环的 Floyd-Warshall 算法 [关闭]

只有五行的算法--floyd-warshall

最短路径的floyd算法的时间复杂度

多源最短路径 – Floyd-Warshall Algorithm

Floyd算法解决多源最短路径问题

Floyd-Warshall 算法-- 最短路径(适合节点密集的图)