Floyd-Warshall

Floyd-Warshall算法(弗洛伊德算法): 求解图中任意两点的最短路径的算法。图可以是有向图或无向图,可以有负权边,但不能有负圈 (负圈上的顶点无最短路径,可无限短)。算法以 3 重循环考察任意顶点 ii 到任意顶点 jj 是否有经过任意顶点 kk 的可松弛路径,即对每一个顶点 kk (外层循环),考察是否有 d(i,k)+d(k,j)<d(i,j)d(i, k) + d(k, j) < d(i, j) ,若有则更新 d(i,j)=d(i,k)+d(k,j)d(i, j) = d(i, k) + d(k, j)

可以这样理解其工作过程。已知确定的任意两点 i,ji, j 间有确定的最短路径 p(i,j)p(i, j) (只要无负圈,必有最短路径),p(i,j)p(i, j) 由多次松弛操作得到。先假设算法是正确的 (详细证明见后述),那么在 p(i,j)p(i, j) 的最后一次松弛后 (通过顶点 yy 松弛),得到 p(i,j)=p(i,y)+p(y,j)p(i, j) = p(i, y) + p(y, j) 。同理,p(i,y)p(i, y)p(y,j)p(y, j)p(i,j)p(i, j) 的两个部分,它们由之前的松弛操作得到。例如松弛顶点 xx 后得到 p(i,y)p(i, y) ,可知 p(i,y)=p(i,x)+p(x,y)p(i, y) = p(i, x) + p(x, y) ,松弛顶点 zz 后得到 p(y,j)p(y, j) ,可知 p(y,j)=p(y,z)+p(z,j)p(y, j) = p(y, z) + p(z, j) 。路径 p(i,j)p(i, j) 的构建过程可以一直拆分溯源到某三个相邻顶点的连接。例如在 p(i,j)p(i, j) 上有三个相邻顶点为 i>a>bi > a > b ,那么在外层循环处理 aa ,中层循环处理 ii ,内层循环处理 bb 时,松弛操作将 i>a>bi > a > b 「连接起来」(因为最短路径上的任意子路径都是最短的,必松弛)。顺着算法执行过程不难看出,算法通过外循环的 kk 来连接边,通过不断连接短路径产生长路径,最终增长为完整的最短路径。


算法过程
  1. 顶点间最短路径 (距离) 矩阵初始化并保存。顶点到自身距离为 0,相邻顶点间距离为边权,不相邻顶点间距离设为 InfinityInfinity 。若要求输出路径本身,则需要另一个最短路径信息矩阵。
  2. 以 3 个循环 (循环操作对象是所有顶点) 考察 iijj 是否可经由 kk 松弛。 k,i,jk, i, j 分别对应外,中,内层循环。
  3. 松弛操作。若 d(i, k) + d(k, j) < d(i, j) ,则 d(i, j) = d(i, k) + d(k, j) ,若有路径信息矩阵,对应更新。

递归输出路径:通过路径信息的矩阵。d[i][j]d[i] [j] 的值是 kk ,即 ii 经过 kkjj ,在每次松弛时更新 kk 。例如有最短路径 i>a>b>c>ji > a > b > c > j 。程序结束后得到的路径信息矩阵如下。

顶点 ii aa bb cc jj
ii nullnull nullnull aa nullnull bb
aa nullnull nullnull nullnull nullnull nullnull
bb nullnull nullnull nullnull nullnull cc
cc nullnull nullnull nullnull nullnull nullnull
jj nullnull nullnull nullnull nullnull nullnull

利用该矩阵,通过递归即可找到 i>a>b>c>ji > a > b > c > j 。递归过程大致如下,顶点输出顺序即为路径顺序。

1
2
3
4
5
6
7
8
i > j, 找到b
i > b,找到a
i > a为null,输出i
a > b为null,输出a
b > j,找到c
b > c为null,输出b
c > j为null,输出c
最后输出j

正确性证明(说明)

该算法的 本质是动态规划 ,以状态转移方程的形式描述如下,其中 dp[k][i][j]dp[k][i][j] 表示 经过前 kk 个顶点的松弛,得到的顶点 ii 到顶点 jj 的最短路径长度 。注意第一维的 kk 表示 kk 个顶点,第二维和第三维表示具体的顶点。

1
2
3
1. 定义: dp[k][i][j] 表示经过前 k 个顶点的松弛,得到的顶点 i 到顶点 j 的最短路径长度。
2. 边界: dp[0][i][j] = Infinity
3. 递推: dp[k][i][j] = min{dp[k-1][i][j], dp[k-1][i][k] + dp[k-1][k][j]}

最短路径 不经过kk 个顶点 (顶点 kk ): dp[k][i][j]=dp[k1][i][j]dp[k][i][j] = dp[k-1][i][j]

最短路径 经过kk 个顶点 (顶点 kk ): dp[k][i][j]=dp[k1][i][k]+dp[k1][k][j]dp[k][i][j] = dp[k-1][i][k] + dp[k-1][k][j]

1
2
3
4
5
6
// floyd核心伪代码
for(k : V)
for(i : V)
for(j : V)
if(d(i, k) + d(k, j) < d(i, j))
d(i, j) = d(i, k) + d(k, j)

补充说明:已知点 i,ji, j 之间的最短路径为 p(i,j)p(i, j) ,那么 p(i,j)p(i, j) 上的任意两点 a,ca, c 的最短路径确定在 p(i,j)p(i, j) 上。反证法简单可证。假设 p(i,j)p(i, j) 上两点 a,ca, c 之间的最短路径经过一不在 p(i,j)p(i, j) 上的顶点 bb ,那 i,ji, j 的最短路径也就不是 p(i,j)p(i, j) ,而是 p(i,a)+p(a,b)+p(b,c)+p(c,j)p(i, a) + p(a, b) + p(b, c)+ p(c, j)


实例分析

下面通过一个例子观察 Floyd 算法的动态规划过程,对其正确性可以有更直观的感受。由于算法总是在整张图上进行处理,展示整张图将使过程变得杂乱,因此我们只选取一条 iijj 的最短路径,展现该路径的构建过程。由于这条路径是随意选取的,其他所有在图上的 任意两点 的路径的构成都是类似的。

设图 GGi,ji, j 间最短路径 ppi>a>b>c>d>e>f>g>h>ji > a > b > c > d > e > f > g > h > j 。最外层循环对该路径上顶点的处理顺序可以是任意顺序,例如 b,h,i,g,a,e,j,f,c,db, h, i, g, a, e, j, f, c, d (分别标上序号 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,表示外层循环处理的先后顺序)。现在我们以溯源的方式从处理最后一个顶点 dd 得到 d(i,j)d(i, j) 开始观察,且只观察程序对上述十个顶点的处理(对其他顶点的处理形成其他路径)。该路径的取得只与三个循环对该路径上的顶点的操作有关,循环对其他顶点的操作不影响结果,因为其他顶点不在该最短路径上,由它们导致的松弛不影响路径 pp ,或者说 pp 会从若干条 iijj 的路径中胜出。

外层循环处理 dd 之后由 d(i,d)+d(d,j)d(i, d) + d(d, j) 的结果得到 iijj 的最短路径长 d(i,j)d(i, j) ,因此 d(i,d)d(i, d)d(d,j)d(d, j) 此时必是已知的。继续溯源,看看 d(i,d)d(i, d)d(d,j)d(d, j) 是如何得到的。到 dd 的路径上 cc 是最后被处理的,处理 cc 时计算 d(i,d)=d(i,c)+d(c,d)d(i, d) = d(i, c) + d(c, d) ,其中 d(c,d)d(c, d) 是边长,这是程序开始时已知, d(i,c)d(i, c) 需要继续溯源, d(i,c)=d(i,a)+d(a,c)d(i, c) = d(i, a) + d(a, c) ,其中 d(i,a)d(i, a) 是边长, d(a,c)=d(a,b)+d(b,c)d(a, c) = d(a, b) + d(b, c)d(a,b)d(a, b)d(b,c)d(b, c) 是边长。其余过程如图,标红处表示相邻的顶点的边长,在程序开始时得到。

可以看出,外层循环处理 i 到 j 的最短路径的所有顶点的过程中,先处理的顶点得到 子路径总能够为后处理的顶点构建更长的子路径 ,直到处理 (最短路径上的) 最后一个顶点时,将两个子路径连接起来形成最终的最短路径,这正是动态规划过程的体现。

不同于 BF 动态规划过程的单串单依赖,Floyd 动态规划是 多依赖 的。也可以描述为某一次的松弛形成的路径 不一定直接作用于下一次松弛 ,而是在之后某一次松弛中发生作用。例如处理 b(1)b(1) 和处理 h(2)h(2) 是外层循环的两次相邻的操作,它们分别产生了两条不相连的子路径 a>b>ca>b>cg>h>jg>h>j 。之后外层循环处理 a(5)a(5)a>b>ca>b>c 增长为 i>a>b>ci>a>b>c ,处理 c(9)c(9) 时增长为 i>a>b>c>di>a>b>c>dg>h>jg>h>j 在外层循环处理 g(4)g(4) 时增长为 f>g>h>jf>g>h>j ,处理 f(8)f(8) 时增长为 d>e>f>g>h>jd>e>f>g>h>j ( d>e>fd>e>f 在外层循环处理 e(6)e(6) 时得到)。最终在处理 d(10)d(10) 时得到 i>a>b>c>d>e>f>g>h>ji > a > b > c > d > e > f > g > h > j

image.png

flyod_demo.gif

总结 Floyd 算法的过程,外层循环执行完第 kk 次,给出由 k+1k+1 条边组成的路径,下一次会利用长度为 1,2,...,k+11, 2, ... , k+1 的路径连接出长度为 k+2k+2 的路径。这就好像将任意两点连成单边线(只要这两点之间存在路径),然后再将两条单边线作为零件连成长度为 2 的线,因为具备所有单边线,所以无论长度为 2 的线是由哪些单边线组成的,都可以找到并连起来。然后再利用长度为 1,2 的线作为零件连成长度为 3 的线,因为无论一条长度为 3 的线是如何构成的,构成它的单边线和 2边线都已具备。以此类推直到连出所有可能长度的线。


时空复杂度

时间复杂度: 3 重循环,O(V3)O(|V|^3)

空间复杂度: 用于存储路径长度信息的二维矩阵 (若需求具体路径,还需路径信息矩阵),为 O(V2)O(|V|^2)