相关笔记

概览

本节介绍 Floyd-Warshall 算法,它解决所有结点对最短路径(APSP)问题,时间复杂度为 ==,空间复杂度为 (就地更新版本)。算法基于动态规划,通过逐步放宽"中间顶点"的约束来逼近最短路径。Floyd-Warshall 算法能够正确处理负权边,并且可以检测负权环==。

要点列表:

  • 定义 为从 且所有中间顶点编号不超过 的最短路径权重
  • 核心递推:
  • 三重循环结构,外层循环变量 为当前允许的最大中间顶点编号
  • 就地更新版本只需 空间
  • 可自然扩展为传递闭包算法和前驱矩阵维护
  • 负权环检测:检查最终矩阵的对角线元素

知识结构总览

flowchart TD
    A["23.2 Floyd-Warshall算法"] --> B["核心递推"]
    A --> C["算法流程"]
    A --> D["正确性证明"]
    A --> E["扩展应用"]
    A --> F["复杂度分析"]

    B --> B1["D^(k)_ij: 中间顶点 ∈ {1,...,k}"]
    B --> B2["递推: min(不经过k, 经过k)"]
    B --> B3["D^(0) = W"]

    C --> C1["三重循环: k, i, j"]
    C --> C2["就地更新: 只需两个矩阵"]
    C --> C3["输出: 最短路径矩阵 D"]

    D --> D1["基于路径最大中间顶点编号"]
    D --> D2["数学归纳法"]
    D --> D3["定理23.4"]

    E --> E1["传递闭包 TRANSITIVE-CLOSURE"]
    E --> E2["前驱矩阵 Pi 的维护"]
    E --> E3["负权环检测: D_ii < 0"]

    F --> F1["时间: Theta(V^3)"]
    F --> F2["空间: Theta(V^2) (就地)"]

核心思想

核心思路

Floyd-Warshall 算法的核心思想是逐步放宽中间顶点的约束。想象你要从城市 i 飞到城市 j,最初只允许直飞(不经过任何中转城市)。然后逐步允许经过城市1中转、经过城市1和2中转、…、直到允许经过所有城市中转。每放宽一个约束,某些路径可能变得更短。最终,当允许经过所有城市中转时,得到的就是真正的最短路径。

1. Floyd-Warshall 递推

Floyd-Warshall 递推公式

设图的顶点编号为 。定义 为从顶点 到顶点 的==所有中间顶点编号均不超过 ==的最短路径权重。

递推关系:

初始条件:

(权重矩阵),表示不允许经过任何中间顶点时的最短路径(只有直连边或 )。

最终结果:

2. 直觉解释

递推的直觉理解

递推公式的含义是:从顶点 i 到顶点 j 且中间顶点不超过 k 的最短路径,只有两种可能:

  1. 不经过顶点 k:那么它就是中间顶点不超过 k-1 的最短路径
  2. 经过顶点 k:那么路径分为两段——从 ik 和从 kj(两段的中间顶点都不超过 k-1

取两者的最小值即可。

生活类比: 假设你在规划从北京到广州的最优路线。一开始你只考虑直达航班。然后允许在上海中转,看看经过上海会不会更便宜。接着允许在武汉中转,看看经过武汉会不会更便宜。每增加一个允许的中转城市,你都重新检查所有城市对,看看新允许的中转城市能否带来更优路线。

3. FLOYD-WARSHALL 伪代码

算法执行流程

  1. 初始化 D^(0) = W(权重矩阵)
  2. k1n 循环,逐步放宽中间顶点约束
  3. 对每对 (i, j):若 D[i][k] + D[k][j] < D[i][j],则更新 D[i][j]
  4. 返回 D^(n) 作为所有结点对最短路径
    A["初始化 D^(0) = W"] --> B["令 k = 1"]
    B --> C["对每对顶点 i, j"]
    C --> D{"D[i][k] + D[k][j] < D[i][j]?"}
    D -- 是 --> E["更新 D[i][j] = D[i][k] + D[k][j]"]
    D -- 否 --> F["保持 D[i][j] 不变"]
    E --> G{"还有下一对 i, j?"}
    F --> G
    G -- 是 --> C
    G -- 否 --> H{"k <= n?"}
    H -- 是 --> I["k = k + 1"]
    I --> C
    H -- 否 --> J["返回 D^(n)"]
FLOYD-WARSHALL(W)
1  n = W.rows
2  D^(0) = W
3  for k = 1 to n
4      let D^(k) = (d^(k)_{ij}) be a new n x n matrix
5      for i = 1 to n
6          for j = 1 to n
7              d^(k)_{ij} = min(d^(k-1)_{ij}, d^(k-1)_{ik} + d^(k-1)_{kj})
8  return D^(n)

伪代码的理解

三重循环的层次:

  • 最外层(k):当前允许的最大中间顶点编号
  • 中间层(i):源顶点
  • 最内层(j):目标顶点

循环顺序的重要性: k 必须在最外层。因为第 k 轮的计算依赖第 k-1 轮的结果,必须先完成所有上一轮的计算,才能正确计算当前轮。

4. 就地更新版本

FLOYD-WARSHALL'(W)
1  n = W.rows
2  D = W
3  for k = 1 to n
4      for i = 1 to n
5          for j = 1 to n
6              d[i][j] = min(d[i][j], d[i][k] + d[k][j])
7  return D

就地更新的正确性

就地更新版本将 直接覆盖写入 ,只需 空间。其正确性基于以下观察:

当计算 时:

  • 中尚未被第 轮更新的值(因为 的循环在 的循环内部, 在第 轮中只被更新一次)
  • :即使 在第 轮中已被更新为 ,由于 ,而 (因为 ,且最短路径权重不超过空路径的权重0),所以 。但使用 代替 不会导致错误,因为如果 ,说明经过 的路径更短,这恰好是我们想要考虑的情况

更精确的分析表明,就地更新是安全的,因为 的值都不会大于 ,因此使用更新后的值只会使结果更优或不变。

5. 定理23.4(正确性)

定理23.4(Floyd-Warshall 算法的正确性)

给定带权有向图 ,其权重函数为 ,权重矩阵为 ,且对所有 。若 不包含从源顶点 可达的负权环,则对所有顶点 ,运行 FLOYD-WARSHALL 后返回的矩阵 满足 (从 的最短路径权重)。

6. 传递闭包

传递闭包(Transitive Closure)

给定有向图 传递闭包 满足: 当且仅当从 存在一条路径(路径长度至少为0,即每个顶点到自身可达)。

传递闭包可以用布尔矩阵 表示,其中 当且仅当从 存在一条所有中间顶点编号不超过 的路径。

算法执行流程

  1. 初始化 T^(0) 为邻接矩阵(有边为 1,无边为 0,对角线为 1
  2. k1n 循环
  3. 对每对 (i, j)T[i][j] = T[i][j] OR (T[i][k] AND T[k][j])
  4. 返回 T^(n) 作为传递闭包
    A["初始化 T^(0): 有边=1, 无边=0"] --> B["令 k = 1"]
    B --> C["对每对顶点 i, j"]
    C --> D{"T[i][k] AND T[k][j] == 1?"}
    D -- 是 --> E["T[i][j] = 1"]
    D -- 否 --> F["保持 T[i][j] 不变"]
    E --> G{"还有下一对 i, j?"}
    F --> G
    G -- 是 --> C
    G -- 否 --> H{"k <= n?"}
    H -- 是 --> I["k = k + 1"]
    I --> C
    H -- 否 --> J["返回 T^(n)"]
TRANSITIVE-CLOSURE(W)
1  n = W.rows
2  for i = 1 to n
3      for j = 1 to n
4          if i == j or (i, j) ∈ E
5              t^(0)_{ij} = 1
6          else
7              t^(0)_{ij} = 0
8  for k = 1 to n
9      let T^(k) = (t^(k)_{ij}) be a new n x n matrix
10     for i = 1 to n
11         for j = 1 to n
12             t^(k)_{ij} = t^(k-1)_{ij} OR (t^(k-1)_{ik} AND t^(k-1)_{kj})
13 return T^(n)

传递闭包与 Floyd-Warshall 的关系

传递闭包是 Floyd-Warshall 算法在布尔半环上的特例:

  • min 替换为 OR
  • + 替换为 AND
  • 替换为 0(FALSE)
  • 将权重矩阵替换为邻接矩阵

传递闭包的时间复杂度同样是 Θ(n³),空间可以优化为 Θ(n²)(就地更新)。

7. 前驱矩阵的维护

前驱矩阵

为了重建最短路径,Floyd-Warshall 算法可以同时维护前驱矩阵 ,其中 是从 的中间顶点不超过 的最短路径上 的前驱顶点。

递推公式:

初始条件:

FLOYD-WARSHALL-WITH-PI(W)
1  n = W.rows
2  D^(0) = W
3  for i = 1 to n
4      for j = 1 to n
5          if i != j and w[i][j] < ∞
6              pi^(0)_{ij} = i
7          else
8              pi^(0)_{ij} = NIL
9  for k = 1 to n
10     let D^(k) be a new n x n matrix
11     let Pi^(k) be a new n x n matrix
12     for i = 1 to n
13         for j = 1 to n
14             if d^(k-1)_{ij} <= d^(k-1)_{ik} + d^(k-1)_{kj}
15                 d^(k)_{ij} = d^(k-1)_{ij}
16                 pi^(k)_{ij} = pi^(k-1)_{ij}
17             else
18                 d^(k)_{ij} = d^(k-1)_{ik} + d^(k-1)_{kj}
19                 pi^(k)_{ij} = pi^(k-1)_{kj}
20 return (D^(n), Pi^(n))

前驱矩阵的使用

得到最终的前驱矩阵后,可以通过递归过程重建从顶点 i 到顶点 j 的最短路径:

  • 若前驱为 NIL,则不存在从 ij 的路径
  • 若前驱为 i,则边 (i, j) 是最短路径的最后一条边
  • 否则,递归输出从 i 到前驱顶点的最短路径,然后输出 j

8. 负权环检测

负权环检测

Floyd-Warshall 算法运行结束后,可以通过检查矩阵 对角线元素来检测负权环:

  • 对某个 成立,则从顶点 出发存在一条负权环可达
  • 若对所有 都有 ,则图中不存在任何顶点可达的负权环

原理: 表示从 的最短路径权重。空路径权重为 0,所以如果 ,说明存在一条从 出发又回到 的路径,其权重为负——这就是一个负权环。

注意区分: 负权边(某条边权重为负)是允许的,负权环(一个环的总权重为负)才是问题所在。负权环会导致最短路径无定义(可以无限绕环使路径权重趋近于 )。

9. 复杂度分析

时间与空间复杂度

时间复杂度: ====

  • 三重嵌套循环,每层
  • 循环体内执行常数次运算
  • 总计

空间复杂度:

  • 非就地版本:(存储 两个矩阵)
  • 就地更新版本:(只需一个矩阵)
  • 若同时维护前驱矩阵:(额外一个 矩阵)

与单源算法运行 次的对比:

方法总时间适用条件
Bellman-Ford 运行 $V$ 次
Dijkstra 运行 $V$ 次
Floyd-Warshall允许负权边

当图为稠密图()时,Floyd-Warshall 的 与 Bellman-Ford 运行 次的 相比优势明显。


补充理解与拓展

Floyd-Warshall 算法的发明历史

Floyd-Warshall 算法的历史是一段有趣的”多重发现”故事:

  1. Bernard Roy(1959年):法国数学家 Bernard Roy 在1959年实际上已经发表了本质相同的算法,用于解决可达性问题(传递闭包)。然而这篇论文在当时并未引起广泛关注。

  2. Stephen Warshall(1962年):美国计算机科学家 Stephen Warshall 于1962年在《Journal of the ACM》上发表了传递闭包算法,用于计算有向图的传递闭包。Warshall 当时在一家名为 Computer Associates 的公司工作。

  3. Robert W. Floyd(1962年):美国计算机科学家 Robert W. Floyd 于1962年独立发表了相同的算法,但将其应用于最短路径问题。Floyd 的论文 “Algorithm 97: Shortest Path” 发表在《Communications of the ACM》上,仅用了不到一页的篇幅。

  4. Peter Ingerman(1962年):Peter Z. Ingerman 对 Floyd 的算法给出了清晰的解释和推广。

由于 Floyd 和 Warshall 的论文影响力最大,算法最终以两人的名字命名。实际上,Bernard Roy 才是最早的发现者,但科学史上这种”多重独立发现”的现象并不罕见。

Robert Floyd 的其他贡献: Floyd 还发明了 Floyd 判圈算法(龟兔赛跑算法,用于检测链表中的环)、Floyd-Steinberg 抖动算法(图像处理),并在程序验证和语义学方面做出了奠基性工作。他于1978年获得图灵奖。

来源:Wikipedia “Floyd-Warshall algorithm”; Grokipedia; CLRS 第4版

传递闭包的实际应用

传递闭包在计算机科学的许多领域都有重要应用:

  1. 社交网络分析:在社交网络中,传递闭包可以回答”A 能否通过朋友链到达 B?“这类可达性问题。例如 Twitter/X 的关注关系图中,传递闭包可以计算信息的潜在传播范围。

  2. 数据库查询优化:SQL 中的递归公共表表达式(Recursive CTE,如 WITH RECURSIVE)本质上就是在计算传递闭包。数据库引擎需要高效计算表之间的可达关系来优化查询执行计划。

  3. 编译器数据流分析:编译器在优化阶段需要分析程序的控制流图(CFG),传递闭包用于计算变量的活跃范围、可达定义、支配关系等。例如,要判断某条语句是否可能被执行,就需要分析控制流图的传递闭包。

  4. 软件工程——死锁检测:在操作系统中,资源分配图(RAG)的传递闭包可以用于检测死锁:如果存在一个进程等待环路,则系统处于死锁状态。

  5. 网络路由:在计算机网络中,路由协议(如 BGP)需要计算网络的可达性信息,这与传递闭包问题密切相关。

来源:Wikipedia “Transitive closure”; CLRS 第4版第23章


易混淆点与辨析

混淆:Floyd-Warshall vs 逐步扩展矩阵乘法

两种算法都是解决 APSP 的动态规划方法,但递推维度不同:

维度Floyd-Warshall逐步扩展矩阵乘法
DP状态:中间顶点 :边数
递推
循环次数恰好 慢速 次,快速
复杂度慢速 ,快速
就地更新可以重复平方版本可以
负权边支持支持

Floyd-Warshall 的优势: 代码极其简洁(三重循环 + 一行递推),常数因子小,无需重复平方就能达到

逐步扩展的优势: 揭示了 APSP 与矩阵乘法的代数联系,为 Strassen 等快速矩阵乘法优化打开了大门。

混淆:就地更新 vs 非就地更新

  • 非就地更新(原始版本):为每个 创建新的矩阵 ,空间为 (存储所有中间矩阵)或 (只保留
  • 就地更新 覆盖 ):只需 空间

就地更新为什么是安全的? 关键在于:当计算 时,需要读取 。即使 已经被更新为 ,由于 (经过 中转不会使路径变长),使用更新后的值不会导致错误——它只是可能发现一条更短的路径。

混淆:负权边 vs 负权环

  • 负权边(negative-weight edge):某条边 的权重 。Floyd-Warshall 和 Bellman-Ford 都能正确处理负权边。
  • 负权环(negative-weight cycle):一个环上所有边权重之和为负。负权环会导致最短路径无定义(因为可以无限绕环使路径权重趋近于 )。
  • Dijkstra 算法既不能处理负权边,也不能处理负权环。
  • Bellman-Ford 和 Floyd-Warshall 能处理负权边,并能检测负权环的存在。

检测方法: Floyd-Warshall 运行后检查 ;Bellman-Ford 运行 轮后检查是否还能松弛。


习题精选

题号题目描述难度
23.2-1对图25.2中的加权有向图运行 Floyd-Warshall 算法,展示每次外层循环得到的矩阵 ⭐⭐
23.2-2如何使用23.1节的技术来计算传递闭包?⭐⭐
23.2-3修改 FLOYD-WARSHALL 过程以计算前驱矩阵 ,并严格证明 是最短路径树⭐⭐⭐
23.2-4证明就地更新版本 FLOYD-WARSHALL’ 的正确性⭐⭐
23.2-5修改前驱矩阵的等号处理方式为 ,这种替代定义是否正确?⭐⭐
23.2-6如何利用 Floyd-Warshall 的输出来检测负权环?
23.2-7使用最高编号中间顶点 重建最短路径,给出递推公式和修改后的算法⭐⭐⭐
23.2-8给出一个 时间的有向图传递闭包算法⭐⭐

视频学习指南

资源主题链接说明
MIT 6.006 Lecture 16Graph APSPhttps://www.youtube.com/watch?v=KXzJ7dOqTqIMIT公开课,涵盖Floyd-Warshall
Abdul BariFloyd-Warshall Algorithmhttps://www.youtube.com/watch?v=oNI0rf2P9gE逐步动画演示,清晰易懂
WilliamFisetFloyd Warshallhttps://www.youtube.com/watch?v=4OQeClyHbew包含代码实现与复杂度分析
Tushar RoyFloyd Warshallhttps://www.youtube.com/watch?v=LmXs8pD7xQ8白板推导,适合理解递推过程
NeetCodeFloyd Warshallhttps://www.youtube.com/watch?v=nV_eKstrsE8实战视角,含LeetCode题目

教材原文

CLRS 第4版 23.2节原文

The Floyd-Warshall algorithm solves the all-pairs shortest-paths problem for a weighted, directed graph. It runs in time. The algorithm is based on a dynamic-programming approach. For every pair of vertices , consider a path from to for which all intermediate vertices are in the set . Let be the weight of a shortest path from to with all intermediate vertices in . The Floyd-Warshall algorithm computes a sequence of matrices , where .

The final matrix contains the shortest-path weights. The algorithm can detect the presence of a negative-weight cycle: if for some vertex , then contains a negative-weight cycle reachable from .


参见Wiki

第23章-所有结点对的最短路径 Floyd-Warshall算法