图论(六):所有节点对的最短路径

所有节点对之间的最短路问题:给定一个加权有向图G=(V,E),对于每一对结点 ( u , v ) ∈ V (u,v)\in V (u,v)V,找到从u到v的最短路径。

  • Input:用邻接矩阵表示有向加权图。

    在这里插入图片描述

  • Output:一个n×n的矩阵 D = ( d i j ) D=(d_{ij}) D=(dij) d i j d_{ij} dij表示从i结点到j节点的最短路径。

一种简单的思路是对集合V中所有的结点使用单源最短路算法,这样就能求得所有结点之间的最短路径,时间复杂度为原时间复杂度×n(n为结点个数),但Dijkstra单源最短路算法不能存在负边,有向无环图的单源最短路算法不能有环,有很多限制。下面总结使用单源最短路算法的求所有节点的最短路径的算法与之后要介绍的最短路算法的限制与时间复杂度:

AlgorithmExtra ConstraintsTime Complexity
Dijkastra"w "≥ 0O ( m n log ⁡ n mn\log n mnlogn)
Bellman-FordO ( m n 2 mn^2 mn2)
Basic “matrix multiplication”no negative weight cycleO ( n 4 n^4 n4)
Improved “matrix multiplication”no negative weight cycleO ( n 3 n^3 n3logn)
Floyd-Warshallno negative weight cycleO ( n 3 n^3 n3)

一 近似矩阵乘法算法与改进

  • 定义: l i j m = l_{ij}^m= lijm=ij最多包含m条边的最短路径。(类似Bellman-Ford中的OPT(i,n))

  • 目标:求出所有可能顶点对(i,j)的 l i j n − 1 l_{ij}^{n-1} lijn1(如果图内有负圈,最短路径一定是最多包含n-1条边的简单路径)

思考最短路问题其实包含着最优子结构,因此可以使用动态规划算法求解,写出Bellman方程:
l i j m = { 0 i f    m = 0    a n d    i = j ∞ i f    m ≠ 0    a n d    i = j m i n 1 ≤ k ≤ n { l i k m − 1 + w k j } i f    m > 0 l_{ij}^m=\left\{\begin{matrix} 0&if\;m=0\;and\;i=j\\ \infty& if\;m\not=0\;and\;i=j\\ \underset{1\leq k\leq n}{min}\{l_{ik}^{m-1}+w_kj\}& if\;m>0\\ \end{matrix}\right. lijm=01knmin{likm1+wkj}ifm=0andi=jifm=0andi=jifm>0
伪代码如下:我们依次计算 L 2 , L 3 , . . . , L n − 1 L^2,L^3,...,L^{n-1} L2,L3,...,Ln1,每次的计算过程需要分别对i,j,k迭代一遍。因此总的时间复杂度为 O ( n 4 ) O(n^4) O(n4)

算法的执行过程如图所示:

初始化一个有向加权图, L 1 L^1 L1就是邻接链表

在这里插入图片描述

迭代计算 L 4 L^4 L4

在这里插入图片描述

为什么说这个算法类似于矩阵的乘法呢,对比该算法与矩阵乘法,有一些相似之处:比如计算的步骤、for循环的次数以及时间复杂度都是 O ( n 4 ) O(n^4) O(n4)

在这里插入图片描述

改进该算法的时间复杂度,我们只对 L i j m − 1 L^{m-1}_{ij} Lijm1感兴趣,而对中间的过程不感兴趣,属于多余计算,如何能减少多余计算而又能获得结果?

  • L ( 1 ) = W ( 邻 接 矩 阵 ) L^{(1)} = W(邻接矩阵) L(1)=W()

  • L ( 2 ) = W 2 = W ⋅ W L^{(2)} = W^2=W\cdot W L(2)=W2=WW

  • L ( 4 ) = W 4 = W 2 ⋅ W 2 L^{(4)} = W^4=W^2\cdot W^2 L(4)=W4=W2W2

  • L ( 8 ) = W 8 = W 4 ⋅ W 4 L^{(8)} = W^8=W^4\cdot W^4 L(8)=W8=W4W4

  • L 2 l g ( n − 1 ) = W 2 l g ( n − 1 ) = W 2 l g ( n − 1 ) − 1 ⋅ W 2 l g ( n − 1 ) − 1 L^{2^{lg(n-1)}} = W^{2^{lg(n-1)}} = W^{2^{lg(n-1)}-1} \cdot W^{2^{lg(n-1)}-1} L2lg(n1)=W2lg(n1)=W2lg(n1)1W2lg(n1)1

因此我们只用ceil(lg(n-1))步就可以计算出 L n − 1 L^{n-1} Ln1,总的时间复杂度缩短到 O ( n 3 lg ⁡ n ) O(n^3\lg n) O(n3lgn)

在这里插入图片描述

二 Floyd-Warshall算法

Floyd-Warshall算法考虑的最短路径的描述与上一节中的 l i j m l_{ij}^m lijm有所不同,Floyd-Warshall算法考虑的是一条最短路径上的中间结点(除去起始结点与终止结点以外的结点)。

假定图G的所有结点 V = { 1 , 2 , . . . , n } V=\{1,2,...,n\} V={12...n},考虑其中一个子集 V 1 = { 1 , 2 , . . . , k } V_1=\{1,2,...,k\} V1={12...k},定义 d i j k d_{ij}^k dijk表示从ij的最短路径的中间结点都取自{1,2,…,k}的权值。

假设从ij的最短路径p的中间结点都取自 V 1 V_1 V1点集,Floyd-Warshall算法利用p与从ij的最短路径的中间结点都取自 V 1 − k V_1-k V1k的关系。具体来说根据k是否出现在最短路径上分为:

  • 结点k不是p上的中间结点,p上的中间结点也都属于 V 1 − k V_1-k V1k,因此 d i j k = d i j k − 1 d_{ij}^k=d_{ij}^{k-1} dijk=dijk1
  • 结点kp上的中间结点,则可以将最短路径分为i->k->j,每一条最短路径的中间结点都属于 V 1 − k V_1-k V1k,因此 d i j k = d i k k − 1 + d k j k − 1 d_{ij}^k=d_{ik}^{k-1}+d_{kj}^{k-1} dijk=dikk1+dkjk1

在这里插入图片描述

因此我们可以写出Bellman方程:
d i j k = { w i j i f    k = 0 m i n { d i j k − 1 , d i k k − 1 + d k j k − 1 } i f    k > 0 d_{ij}^k=\left\{\begin{matrix} w_{ij}&if\;k=0\\ {min}\{d_{ij}^{k-1},d_{ik}^{k-1}+d_{kj}^{k-1}\}& if\;k>0\\ \end{matrix}\right. dijk={wijmin{dijk1,dikk1+dkjk1}ifk=0ifk>0
因此算法伪代码如下:

在这里插入图片描述

Floyd-Warshall算法的运行时间由三层嵌套的for循环决定,每次循环执行时间为 O ( 1 ) O(1) O(1),总的运行时间为 O ( n 3 ) O(n^3) O(n3)。c++实现如下:

//
// Created by HP on 2021/12/1.
//
#include <iostream>
#include <cstring>
using namespace std;

#define INF 1000000000;

int V,E;
int Dis[100][100];
int Graph[100][100];

void Floyd_WarShall(){
   //L0 = W 初始化为邻接矩阵
   for(int i=1;i<=V;i++)
       for(int j=1;j<=V;j++)
           Dis[i][j] = Graph[i][j];

   for(int k=1;k<=V;k++){
       for(int i=1;i<=V;i++){
           for(int j=1;j<=V;j++) {
                //Dis[i][j] = min{Dis[i][j],Dis[i][k]+Dis[k][j]}
                if(Dis[i][j] > Dis[i][k]+Dis[k][j])
                    Dis[i][j] = Dis[i][k]+Dis[k][j];
           }
       }
   }


}

int main(){
    cout<<"V E?"<<endl;
    cin>>V>>E;
    for(int i=1;i<=V;i++){
        for(int j=1;j<=V;j++) {
            if (i != j)
                Graph[i][j] = Dis[i][j] = INF
            else
                Graph[i][j] = Dis[i][j] = 0;
        }
    }

    for(int i=1;i<=E;i++){
        int from,to,w;
        cin>>from>>to>>w;
        Graph[from][to] = w;
    }

    Floyd_WarShall();

    for(int i=1;i<=V;i++){
        for(int j=1;j<=V;j++) {
            cout<<Dis[i][j]<<" ";
        }
        cout<<endl;
    }
}


测试用例:

在这里插入图片描述

5 9
1 2 3
3 2 4
1 3 8
2 5 7
2 4 1
1 5 -4
5 4 6
4 3 -5
4 1 2

任意节点对之间的最短距离矩阵的运行结果如图:

在这里插入图片描述

  • 2
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Sunburst7

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值