C++算法:多源最短路径(Floyd)


前言

前文单源最短路径Dijkstra中我们讨论了如何解决有向无环图的最短路径问题,Dijkstra只能解决一个起始点的问题,如果要解决每个顶点到任一顶点的最短路径呢?一个方法就是再循环一次,以每个顶点作为起点就可以了嘛,虽然不可避免的会有部分重复工作,但确实能解决,就是代码更复杂了。有没有一个优雅点的办法呢?今天要介绍的算法Floyd(弗洛伊德,好熟悉的名字~)就是一个优雅的解决办法。它与用Dijkstra循环每个顶点的时间复杂度差不多O(n3),但是代码极简。


一、Floyd算法

Floyd的思想就是前文提到的最优子结构。Floyd算法利用了动态规划的思想。设有向图G=(V,E),V是定点集,取某个顶点k。考虑顶点的一个子集{1,2,3, … k}。对任意一对顶点i,j∈V, 考察从i到j且中间顶点皆属于集合{1,2,3, … k}的所有的路径,设p是其中的一条最短路径 (p没有回路)。那么我们考虑两种情况:
(1) 如果k不是路径p的中间顶点,则P的所有中间顶点皆在集合{1, 2, 3, … k}中。因此,从顶点i到顶点j且满足中间顶点全在集合{1, 2, 3, … k}中的一条最短路径同样是从顶点i到顶点j且满足中间顶点均在集合 {1, 2, 3, … k} 中的一条 最短路径。这一点可以帮我们不停地缩小集合的范围。
(2)如果k是路径P的中间顶点,那么可将p分解为子路径p1<i,k>和子路径p2<k,j>,我们知道,最短路径的子路径也是最短路径,所以子路径p1和子路径p2分别是顶点i到k和顶点k到j且满足所有中间顶点均在集合{1,2,3, … k}上的最短路径。
如果我们用(dij n) 表示从顶点i到顶点j,且满足所有中间顶点属于集合{1,2,3, … k}的一条最短路径的权值和,那么上面两点就可以抽象成如下的递归式:

其中wij表示图中顶点i到j之间的边的权值,如果无边直接相连,则为+∞。k=0的时候,说明顶点i和顶点j之间没有中间点。由于是对任意路径,所以当k=n的时候,该图的每对顶点的最短路径的权值就可由矩阵 Dn = (dij n) 表示。
上面的递归式就是Floyd算法的核心思想,它说明Floyd算法是从全局的观点出发来找任意两个顶点间的最短路径。每取一个k值,都会遍历一次整个图矩阵,找出中间顶点在{1,2,3, … k}中所有的最短路径,随着k值的增加,算法的结果逐渐趋近于最终结果,当k=n时,就是我们需要的结果。这里大家要有一个意识,就是直接由边相连的顶点之间k=0的路径权值不一定小于经过多个点之后的路径的权值。

有了上述理论思想,我们继续用前文使用过的图来看看:

二、代码实现

我们继续在前文的代码改巴改巴,原矩阵就注释了。用一个path矩阵来代替,因为这个矩阵会被改写。再用一个二维数组pre来存储前面理论中讨论的 k,因为这个值和 i、j 有关,所以要用二维数组。

代码如下(示例):

#include <iostream>
#include <vector>
#include <stack>

using namespace std;

class Graph{
    private:
        int vertex;      //顶点数
        //int** matrix;    //有向图关系矩阵
        int** path;    //存储关系矩阵
        int** pre;         //存储中间节点k

    public:
        const int maxium = 10000;                //最大值,表示不存在的边
        Graph(const int edges, const int nodes, int arr[][3]){
            vertex = nodes;  
            //matrix = new int* [vertex];          //生成有向图关系矩阵
            pre = new int* [vertex];
            path = new int* [vertex];             //生成有向图关系矩阵
            for (int i=0; i<vertex; ++i){
                pre[i] = new int[vertex];
                path[i] = new int[vertex];
                //matrix[i] = new int[vertex];
                for (int j=0; j<vertex; j++){
                    //matrix[i][j] = maxium;
                    path[i][j] = maxium;
                    pre[i][j] = -1;
                }
            }
            for (int i=0; i<edges; ++i){                    //生成有向图关系,maxium为不连接
                //matrix[arr[i][0]][arr[i][1]] = arr[i][2];
                path[arr[i][0]][arr[i][1]] = arr[i][2];
            }
        }

        ~Graph(){
            delete[] path;
            //delete[] matrix;
            delete[] pre;
        }

        void floyd(int s, int end){
            for (int k=0; k<vertex; ++k){
                for (int i=0; i<vertex; ++i){
                    for (int j=0; j<vertex; ++j){
                        if (path[i][k] + path[k][j] < path[i][j]){
                            path[i][j] = path[i][k] + path[k][j];
                            pre[i][j] = k;
                        }
                    }
                }
            }    
            show(s, end);
        }

        void show(int start, int end){  //显示路径,单源,多源直接for循环调用这个函数
            int k = pre[start][end];
            if (k != -1){
                show(start, k);
                cout << k << " ";
                show(k, end);
            }
        }
};

二维数组path,用于存储各顶点对的权值,即从i到j的最短路径权值。在Floyd函数中,通过三重循环遍历所有顶点对,更新path数组中的值。同时,还有一个二维数组pre,用于存储中间节点k。在Floyd函数中,当发现从i到j经过k比直接从i到j更短时,就更新path[i][j]和pre[i][j]的值。最后,通过show函数输出从起点到终点的最短路径。Floyd方法已经计算了所有顶点对之间的最短路径,如果要查看这个路径,循环调用show方法即可。


总结

Floyd算法的时间复杂度是O(n3),但是实现非常简单优雅,核心代码才几行。用示例图数据测试:

int main(){
    int arr[][3] = {{0,1,8},{0,3,16,},{0,4,7},{1,3,9},{1,5,5},{2,9,2},
                   {3,2,1},{3,6,10},{3,8,12},{4,7,5},{4,3,9},{4,8,7},{5,3,2},
                   {5,2,11},{6,2,13},{6,9,2},{7,6,8},{8,7,1},{8,6,6}};

    Graph t(19, 10, arr);
    t.floyd(0, 9);
    return 0;
}

结果是:1 5 3 2 ,这里偷懒没有写起点和终点,结果和Dijksrta算法得到的一致。

  • 4
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

无证的攻城狮

如本文对您有用,大爷给打个赏!

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

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

打赏作者

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

抵扣说明:

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

余额充值