单源最短路径
我们可以用已经知道的算法,来对所有点计算一次最短路径
无权重的图
使用V V V 次BFS算法,时间复杂度是O ( V 2 + V E ) O(V^2+VE) O ( V 2 + V E )
非负权重的图
使用V V V 次D i j k s t r a Dijkstra D i j k s t r a 算法
线性数组:O ( V 3 + V E ) = O ( V 3 ) O(V^3+VE)=O(V^3) O ( V 3 + V E ) = O ( V 3 )
二叉堆:O ( V E l g V ) O(VElgV) O ( V E l g V )
斐波那契堆:O ( V 2 l g V + V E ) O(V^2lgV+VE) O ( V 2 l g V + V E )
含有负权重的图
使用V V V 次B e l l m a n − F o r d Bellman-Ford B e l l m a n − F o r d 算法,时间复杂度是O ( V 4 ) O(V^4) O ( V 4 )
看时间复杂度,以上的方法都不是很好,所以我们需要更好的方法
Floyd-Warshall
Floyd-Warshall的本质是使用动态规划,其步骤为
分析最优解结构
递归定义最优解的值
自底向上计算最优解的值
根据计算出的最优解的值构造最优解
此算法可以计算有权重为负的边,但是不能计算存在负权重的环路。
流程
我们假定一个图G = ( E , V ) G=(E, V) G = ( E , V ) ,其中V = { 1 , 2 , 3 , ⋯ , n } V=\lbrace1,2,3,\cdots,n\rbrace V = { 1 , 2 , 3 , ⋯ , n } , 我们选取任意一个子集V ′ V' V ′ ,使得
V ′ = { 1 , 2 , 3 , . . . , k } V' = \lbrace1,2,3,...,k\rbrace
V ′ = { 1 , 2 , 3 , . . . , k }
V ′ ∈ V V' \in V
V ′ ∈ V
如图,蓝色的集合就是V ′ V' V ′
对于任意结点i , j ∈ V i,j \in V i , j ∈ V ,我们考虑所有 从结点i i i 到结点j j j 的所有中间结点都∈ V ′ \in V' ∈ V ′ 的路径
这里的中间结点 指的是:简单路径p = { n 1 , n 2 , n 3 , ⋯ , n k } p=\lbrace n_1,n_2,n_3,\cdots,n_k\rbrace p = { n 1 , n 2 , n 3 , ⋯ , n k } 上除结点v 1 , v k v_1, v_k v 1 , v k 结点以外点任意结点。
这里,我们设p p p 为这些路径中权重最小的一条边,我们需要通过p p p 和集合{ 1 , 2 , 3 , ⋯ , k − 1 } \lbrace1,2,3,\cdots,k-1\rbrace { 1 , 2 , 3 , ⋯ , k − 1 } 的关系,来寻找对于所有结点点最短路径。
总共分2种情况,我们先定义一个变量d i j ( k ) d^{(k)}_{ij} d i j ( k ) :表示从结点i i i 到j j j 的所有中间结点均∈ { 1 , 2 , 3 , ⋯ , k } \in \lbrace1,2,3,\cdots,k\rbrace ∈ { 1 , 2 , 3 , ⋯ , k } 的一条最短路径的权重。
结点k k k 不是路径p p p 的中间结点
现在,路径p p p 的中间结点都属于集合{ 1 , 2 , 3 , ⋯ , k − 1 } \lbrace1,2,3,\cdots,k-1\rbrace { 1 , 2 , 3 , ⋯ , k − 1 } ,就可以推出
d i j ( k ) = d i j ( k − 1 ) d^{(k)}_{ij} = d^{(k-1)}_{ij}
d i j ( k ) = d i j ( k − 1 )
结点k k k 是路径p p p 的中间结点
现在,路径p p p 的被中间结点k k k 划分为p 1 , p 2 p_1, p_2 p 1 , p 2 2条路径,其中每条路径的结点都∈ { 1 , 2 , 3 , ⋯ , k − 1 } \in \lbrace 1, 2, 3, \cdots, k - 1 \rbrace ∈ { 1 , 2 , 3 , ⋯ , k − 1 } ,就可以推出
d i j ( k ) = d i k ( k − 1 ) + d k j ( k − 1 ) d^{(k)}_{ij} = d^{(k-1)}_{ik} + d^{(k-1)}_{kj}
d i j ( k ) = d i k ( k − 1 ) + d k j ( k − 1 )
递归定义最优解
根据上方,我们可以定义出d i j ( k ) d^{(k)}_{ij} d i j ( k ) 与d i j ( k − 1 ) d^{(k-1)}_{ij} d i j ( k − 1 ) 的关系,从而得到一个递归解
d i j ( k ) = { ϖ i , j k = 0 m i n ( d i j ( k − 1 ) , d i k ( k − 1 ) + d k j ( k − 1 ) ) k > = 1 d^{(k)}_{ij} =
\begin{cases}
\varpi_{i,j} & k = 0 \\
min(d^{(k-1)}_{ij}, d^{(k-1)}_{ik} + d^{(k-1)}_{kj}) & k >= 1 \\
\end{cases}
d i j ( k ) = { ϖ i , j m i n ( d i j ( k − 1 ) , d i k ( k − 1 ) + d k j ( k − 1 ) ) k = 0 k > = 1
我们还规定
ϖ i , j = { 0 i = j ϖ i , j i ≠ j ∞ i 与 j 不连通 \varpi_{i,j} =
\begin{cases}
0 & i = j \\
\varpi_{i,j} & i \neq j \\
\infty & i \text{与} j \text{不连通} \\
\end{cases}
ϖ i , j = ⎩ ⎪ ⎨ ⎪ ⎧ 0 ϖ i , j ∞ i = j i = j i 与 j 不连通
这里k > = 1 k >= 1 k > = 1 的情况,通俗点说明就是当路径p p p 的被中间结点k k k 划分为p 1 , p 2 p_1, p_2 p 1 , p 2 的2条路径时,且这2条路径加起来比d i j ( k − 1 ) d^{(k-1)}_{ij} d i j ( k − 1 ) 的权重小的话,那么就选d i k ( k − 1 ) + d k j ( k − 1 ) d^{(k-1)}_{ik} + d^{(k-1)}_{kj} d i k ( k − 1 ) + d k j ( k − 1 ) ,大的话就选d i j ( k − 1 ) d^{(k-1)}_{ij} d i j ( k − 1 ) 。要注意,这里的k k k ,我们是不知道在哪里的,需要对集合k ∈ V k \in V k ∈ V 进行遍历。
C语言代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 int **floyd_warshall (int graph[][MAX_NODE], int length) { int n = length; int **L = (int **)malloc (sizeof (int *) * n); for (int i = 0 ; i < n; i++) L[i] = (int *)malloc (sizeof (int ) * n); for (int i = 0 ; i < n; i++) for (int j = 0 ; j < n; j++) L[i][j] = graph[i][j]; for (int k = 0 ; k < n; k++) for (int i = 0 ; i < n; i++) for (int j = 0 ; j < n; j++) L[i][j] = min (L[i][j], L[i][k] + L[k][j]); return L; }
代码的3-6行,我们创建一个二维数组L[i][j]
,来记录当前i i i 与j j j 的路径的长度,而代码的12-15行,我们对每一个k结点进行松弛(relax)操作,如果通过k结点到达i更短,那么就更新当前路径权重。
关于k的循环的顺序问题
Floyd-Warshall里面有个定理:
定理: 假设i i i 和j j j 之间的最短路径上的结点集里(不包含i i i ,j j j ),编号最大的一个是x x x .那么在外循环k = x k=x k = x 时,L[i][j]
肯定得到了最小值。
我们可以通过这张图来看,
首先是k=1
,通过点1并不会有什么改变,所以什么都不会变
然后是k=2
,通过点2,我们会发现L[1][3]
取得了最小值,此时L[1][2]
和L[2][3]
已经是最小值了。
然后是k=3
,通过点3,我们会发现L[1][4]
取得了最小值=此时L[1][2]
和L[2][3]
和L[3][4]
已经是最小值了。
然后是k=4
,通过点4,我们会发现L[1][5]
取得了最小值,此时L[1][2]
和L[2][3]
和L[3][4]
和L[4][5]
已经是最小值了。
即使把5和2或者其他任意的顺序交换也会得到同样的结果。所以对k的循环必须在最外层。这也就是为什么本来是3层循环,应该是三维数组来储藏,但是最后却只用了二维数组就做到了的原因,因为上一步计算出点最小值,被用于了下一步,而不需要重新再计算。这也就是动态规划的基本。
求最短路径
上面我们只是把最短路径点权重计算出来了,但是我们还不知道最短路径具体是什么,通过关于k的循环的顺序问题 ,我们知道了,当我们在本次通过x
取得L[1][k]
的最小值的时候,L[1][x]
已经取得了最小值。所以我们可以记录前驱结点,也就是k
的前驱结点就是x
,同理,x
也有自己对应的前驱结点,这样递归点找下去,即可找到最短路径。
于是
k = 0 k=0 k = 0
π i , j ( 0 ) = { N I L i = j o r ϖ i , j = ∞ i i ≠ j a n d ϖ i , j ≠ ∞ \pi^{(0)}_{i,j} =
\begin{cases}
NIL & i = j\ or\ \varpi_{i,j} = \infty \\
i & i \neq j\ and\ \varpi_{i,j} \neq \infty \\
\end{cases}
π i , j ( 0 ) = { N I L i i = j o r ϖ i , j = ∞ i = j a n d ϖ i , j = ∞
k > 0 k>0 k > 0
π i , j ( k ) = { π i , j ( k − 1 ) d i j ( k ) = d i j ( k − 1 ) π k , j ( k − 1 ) d i j ( k ) = d i k ( k − 1 ) + d k j ( k − 1 ) \pi^{(k)}_{i,j} =
\begin{cases}
\pi^{(k-1)}_{i,j} & d^{(k)}_{ij} = d^{(k-1)}_{ij} \\
\pi^{(k-1)}_{k,j} & d^{(k)}_{ij} = d^{(k-1)}_{ik} + d^{(k-1)}_{kj} \\
\end{cases}
π i , j ( k ) = { π i , j ( k − 1 ) π k , j ( k − 1 ) d i j ( k ) = d i j ( k − 1 ) d i j ( k ) = d i k ( k − 1 ) + d k j ( k − 1 )
求最短路径的C语言代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 int **floyd_warshall (int graph[][MAX_NODE], int length) { int n = length; int **L = (int **)malloc (sizeof (int *) * n); int **P = (int **)malloc (sizeof (int *) * n); for (int i = 0 ; i < n; i++) { L[i] = (int *)malloc (sizeof (int ) * n); P[i] = (int *)malloc (sizeof (int ) * n); } for (int i = 0 ; i < n; i++) { for (int j = 0 ; j < n; j++) { L[i][j] = graph[i][j]; P[i][j] = j; } } for (int k = 0 ; k < n; k++) { for (int i = 0 ; i < n; i++) { for (int j = 0 ; j < n; j++) { if (L[i][j] > L[i][k] + L[k][j]) { L[i][j] = L[i][k] + L[k][j]; P[i][j] = k; } } } } return P; }
矩阵乘法
我们来看看矩阵乘法的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 void multiply (int mat1[][N], int mat2[][N], int res[][N]) { int i, j, k; for (i = 0 ; i < N; i++) { for (j = 0 ; j < N; j++) { res[i][j] = 0 ; for (k = 0 ; k < N; k++) res[i][j] += mat1[i][k] * mat2[k][j]; } } }
可以看出,上方的求最短路径点代码和矩阵点乘法非常相似。那么矩阵乘法也可以得到应用。假设我们有了恰好经过k − 1 k-1 k − 1 条边的方案数矩阵B ∈ R n × n B \in \mathbb{R}^{n\times n} B ∈ R n × n ,还有图的临接矩阵A ∈ R n × n A \in \mathbb{R}^{n\times n} A ∈ R n × n ,那么B × A B \times A B × A 就是恰好经过 K 条边互相可达的方案数矩阵
定理: 设G = ⟨ V , E , ψ ⟩ G = \langle V, E,\psi \rangle G = ⟨ V , E , ψ ⟩ 是一简单有向图,并且A A A 是G G G 的临接矩阵,对于m = 1 , 2 , 3 , ⋯ m=1,2,3,\cdots m = 1 , 2 , 3 , ⋯ 来说,矩阵A m A^m A m 中的元素a i j a_{ij} a i j 的值,等于从v i v_i v i 到v j v_j v j 长度为m的路径数目。
证:对于m m m 进行归纳证明。当m = 1 m=1 m = 1 时,很明显A = A A=A A = A
设矩阵A l A^l A l 中的元素( i , j ) (i, j) ( i , j ) 值是a i j l a^l_{ij} a i j l ,且对于m = 1 m=1 m = 1 来说结论为真。
因为A l + 1 = A l A A^{l+1} = A^lA A l + 1 = A l A ,所以应有
a i j l + 1 = ∑ k = 1 n a i k l a k j a^{l+1}_{ij}=\sum^n_{k=1}a^l_{ik}a_{kj}
a i j l + 1 = k = 1 ∑ n a i k l a k j
这里v k v_k v k 是倒数第二个结点,因此,a i j l + 1 a^{l+1}_{ij} a i j l + 1 应是从结点v i v_i v i 出发,经过任意的倒数第二个结点到v j v_j v j 的长度为l + 1 l+1 l + 1 的路径总数。
我们把矩阵乘法用在求恰好经过K K K 条边最短路径上
1 2 3 4 5 6 7 8 9 10 11 12 13 void multiply (int A[][N], int Al[][N], int res[][N]) { int i, j, k; for (i = 0 ; i < N; i++) { for (j = 0 ; j < N; j++) { res[i][j] = 0 ; for (k = 0 ; k < N; k++) res[i][j] = Al[i][k] + A[k][j]; } } }
其中Al[i][k]
就相当与a i k l a^{l}_{ik} a i k l ,那么a i k l + a k j = a i j l + 1 a^{l}_{ik} + a_{kj} = a^{l+1}_{ij} a i k l + a k j = a i j l + 1 ,但是请注意,这里是从假设我们有了恰好经过k − 1 k-1 k − 1 条边的方案数矩阵B ∈ R n × n B \in \mathbb{R}^{n\times n} B ∈ R n × n 开始的,而当前这一步的时间复杂度就达到了O ( n 3 ) O(n^3) O ( n 3 ) ,由于要求n n n 个点,所以总的时间复杂度达到O ( n 4 ) O(n^4) O ( n 4 ) 。
重复平方
我们对A m , m ∈ R A^m, m \in R A m , m ∈ R 的结果并不感兴趣,因为对于n n n 个结点点图,两点之间的最短距离最大只有n − 1 n-1 n − 1 条边,所以我们真正关心的是A n − 1 A^{n-1} A n − 1 ,我们可以通过重复平方的方法来改良当前的算法
A = A A 2 = A ∗ A A 4 = A 2 ∗ A 2 A 8 = A 4 ∗ A 4 ⋮ A 2 l g ( n − 1 ) = A 2 l g ( n − 1 ) ∗ A 2 l g ( n − 1 ) − 1 \begin{aligned}
A &= A
\\
A^2 &= A * A
\\
A^4 &= A^2 * A^2
\\
A^8 &= A^4 * A^4
\\
&\vdots
\\
A^{2lg(n-1)} &= A^{2lg(n-1)} * A^{2lg(n-1) - 1}
\end{aligned}
A A 2 A 4 A 8 A 2 l g ( n − 1 ) = A = A ∗ A = A 2 ∗ A 2 = A 4 ∗ A 4 ⋮ = A 2 l g ( n − 1 ) ∗ A 2 l g ( n − 1 ) − 1
由于2 l g ( n − 1 ) ≥ n − 1 2lg(n-1) \geq n-1 2 l g ( n − 1 ) ≥ n − 1 ,所以A 2 l g ( n − 1 ) = A n − 1 A^{2lg(n-1)} = A^{n-1} A 2 l g ( n − 1 ) = A n − 1 ,这样优化后时间复杂度从O ( n 4 ) O(n^4) O ( n 4 ) 降到O ( n 3 l g n ) O(n^3lgn) O ( n 3 l g n ) ,和Floyd Warshall(O ( N 3 ) O(N^3) O ( N 3 ) )还是有很大的差距的。
总结
Floyd Warshall利用动态规划,可以在O ( N 3 ) O(N^3) O ( N 3 ) 就能求得所有结点对的最短路径。
而通过临接矩阵相乘来计算,时间复杂度比Floyd Warshall高得多,既是优化后也提升不大,但是其中的思想还是可以学习的。