最短路径算法汇总「建议收藏」

最短路径算法汇总「建议收藏」1、Floyd-Warshall算法A、算法基本思想   在有向连通图中,从任意顶点i到顶点j的最短路径,可以看做从顶点i出发,经过m个顶点中转,到达j的最短路程。最开始可以只允许经过”1”号顶点进行中转,接下来只允许经过”1”号顶点和”2”号顶点进行中转……允许经过”1”~”m”号顶点进行中转,求任意两顶点的最短路程。B、算法实现for(intk=1;k<=n;

大家好,又见面了,我是你们的朋友全栈君。

1、Floyd-Warshall算法

A、算法基本思想

      在有向连通图中,从任意顶点i到顶点j的最短路径,可以看做从顶点i出发,经过m个顶点中转,到达j的最短路程。最开始可以只允许经过”1”号顶点进行中转,接下来只允许经过”1”号顶点和”2”号顶点进行中转……允许经过”1”~”m”号顶点进行中转,求任意两顶点的最短路程。


B、算法实现

        for (int k = 1; k <=n; k++) {
            for (int i = 0; i <= n; i++) {
                for (int j = 0; j < n; j++) {
                    if(e[i][j] > e[i][k] + e[k][j]){
                        e[i][j] = e[i][k] + e[k][j]
                    }
                }
            }
        }

C、算法说明

      Floyd-Warshall算法的时间复杂度是O(n^3),但是我们发现它仅仅下只有五行代码,实现起来非常容易。如果时间复杂度要求不高,使用该算法求解两点间的最短路径或指定点到其余点的最短路径是可行的选择。


2、Dijkstra算法

A、算法基本思想

      每次找到离源顶点最近的顶点,然后以该顶点为中心扩展,最终得到源顶点到其余所有顶点的最短路径。


B、算法实现

public class Main {
    int[] book = new int[100]; //标识哪些顶点在最短路程顶点集合中
    int min; //各顶点到指定顶点的距离
    int u; //到指定顶点距离最短的点
    int inf = Integer.MAX_VALUE;
    int[] dis = new int[10]; //存储指定顶点到各顶点的距离
    int[][] e = new int[10][10]; //有向图组成的二维数组

    public void dijkstra(int n) {
        for (int i = 1; i <= n - 1; i++) {
            //找到距离1号顶点的最近顶点
            min = inf;
            for (int j = 1; j <= n; j++) {
                if (book[j] == 0 && dis[j] < min) {
                    min = dis[j];
                    u = j;
                }
            }

            //顶点u距离1号顶点距离最近
            book[u] = 1;
            for (int v = 1; v < n; v++) {
                //从顶点u出发寻找边e[u][v],如果顶点1到顶点v的距离大于经过顶点u到顶点v的距离,则将顶点
                //1到顶点v的距离改变
                if (e[u][v] < inf) {
                    if (dis[v] > dis[u] + e[u][v]) {
                        dis[v] = dis[u] + e[u][v];
                    }
                }
            }
        }
    }
}

C、算法说明

      Dijkstra算法稍有些复杂,这里简单阐述下基本步骤:1、首先将所有的顶点分为两部分:已知最短路径的顶点集合P和未知最短路径的顶点集合Q。开始时,已知最短路径的顶点集合P中只有源点一个顶点。使用数组book记录哪些顶点在集合P中。对于某个顶点i,如果book[i]为1则表示这个顶点在集合P中,否则则在集合Q中。2、设置源点s到自己的的最短路径为0,即有dis[s]=0。若存在有源点能直接到达的顶点i,则把dis[i]设定为e[s][i]。同时把所有其它顶点的最短路径设为无穷大。3、在集合Q的所有顶点中选取一个离源点s最近的顶点u(即dis[u]最小)加入到集合P。并考察所有以点u为起点的边。如果存在一条从u到v的边,那么可以通过将边u->v添加到尾部拓展一条从s到v的路径,这条边的长度是dis[u]+e[u][v]。如果这个值比目前已知的dis[v]要小,可以用新值来替代当前dis[v]中的值。4、重复步骤3直到集合Q为空,算法结束。最终dis数组中的值就是源点到所有顶点的最短路径。·


3、Bellman-Ford算法

A、算法基本思想

      Dijkstra算法虽好,但不能解决带有负权边的图。Bellman-Ford算法可以完美地解决带负权边的图。对于有n个顶点的边,对其所有的边进行n-1次“松弛操作”。下面的代码中,外循环一共循环n-1次,内循环循环了m次。dis数组用来记录源点到其余各个顶点的最短路径。u、v、w三个数组是用来记录边的信息。例如第i条边存储在u[i]、v[i]和w[i]中,表示从顶点u[i]到顶点v[i]这条边(u[i]->v[i])的权值为w[i]。

      算法实现的最后两行代码表明:看能否通过u[i]->v[i](权值为w[i])这条边,使得1号顶点到v[i]号顶点的距离变短。即1号顶点到u[i]号顶点的距离dis[u[i]]加上u[i]->v[i]这条边的(权值为w[i])的值是否比原先1号顶点到v[i]号顶点的距离(dis[v[i]])要小。


B、算法实现

        //m为边的条数,n为顶点个数
        for(int k =1; k< n-1; k++) {
            for(int i = 1; i <= m; i++) {
                if(dis[v[i]] > dis[u[i] +w[i]) {
                    dis[v[i]] = dis[u[i]] + w[i];
                }
            }
        }

C、算法说明

      对于n个顶点的图,只需要进行n-1次整体的遍历即可。因为在一个含有n个顶点的图中,任意两点的最短路径最多包含n-1条边。即第1轮对所有的边进行松弛后,得到的是1号顶点“只能经过一条边”到达其余各顶点的最短路径长度。第2轮在对所有的边进行松弛之后,得到的是从1号顶点“最多经过两条边”到达其余各顶点的最短路径长度。如果进行k轮的话,得到的就是1号顶点“最多经过k条边”到达其余各顶点的最短路径长度。

      在每次进行松弛后,就会有一些顶点已经求得最短路径,此后这些顶点的估计值就会一直保持不变,不再会受后续松弛操作的影响,但是每次还要判断是否需要松弛,这会浪费一些时间。能否每次仅对最短路径估计值发生变化的顶点的所有出边执行松弛呢?这就涉及到Bellman-Ford的队列优化。

      注:上文没有举例详细阐述Bellman-Ford是如何解决负权边的,感兴趣可以百度相关文章学习下。


4、Bellman-Ford算法队列优化

A、算法基本思想

      每次仅对最短路径发生变化了的点的相邻边执行松弛操作,通过一个队列来维护当前哪些点的最短路程发生变化。每次选取队首顶点u,对顶点u的所有出边进行松弛操作。例如u->v这条边,如果通过u->v这条边使得源点到顶点v的最短路程变短,即dis[u]+e[u][v]< dis[v],且顶点v不在当前队列中,则将顶点v放入队尾。这里需要一个数组来判重(如果顶点存在于队列中则不加入)。在对顶点u的所有出边松弛后,将顶点v出队列,然后不断从队列中取出新的队首顶点再进行如上操作,直至队列为空。


B、算法实现

        //伪代码
        int head=1;
        int tail=1;
        int que[101]={
  
  0};
        que[tail]=1;
        int k;
        //1号顶点入队
        que[tail]=1;tail++;
        book[1]=1;//标记1号顶点已经入队
        while(head < tail) {
            k = first[que[head]];//当前需要处理的队首顶点
            while(k != -1) {
                if(dis[v[k] > dis[u[k]] + w[k]){ //判断是否松弛成功
                    dis[v[k] = dis[u[k]] + w[k];
                    //book数组用来判断顶点v[k]是否在队列中
                    if(book[v[k]]==0) { //表明不在队列中,将v[k]加入队列中
                        que[tail]=v[k]; //将顶点v[k]入队
                        tail++;
                        book[v[k]]=1;//标记顶点v[k]已经入队
                    }
                    k=next[k];
                }
            }
            //出队
            book[que[head]]=0;
            head++;
        }

C、算法说明

      使用队列优化的Bellman-Ford算法在形式上和广度优先搜索非常类似,不同的是在广度优先搜索时一个顶点出队后就不会再次进行队列中,而这里一个顶点很可能在出队列后再次被放入队列,也就是当一个顶点的最短路程估计值变小后,需要对其所有出边进行松弛,但是如果这个顶点的最短路程估计值再次变小,仍然需要对其所有出边再次进行松弛,这样才能保证相邻顶点的最短路程估计值同步更新。队列优化的Bellman-Ford算法的时间复杂度在最坏的情况下是O(NM)。其可以通过某个点进入队列的次数来判断是否存在负环,如果进入次数超过n次,则说明存在负环。


5、算法对比分析


  Floyd Dijkstra Bellman-Ford 队列优化的Bellman-Ford
空间复杂度 O(N2) O(M) O(M) O(M)
时间复杂度 O(N3) O((M+N)logN) O(MN) 最坏O(MN)
试用情况 稠密图和顶点关系密切 稠密图和顶点关系密切 稀疏图和边关系密切 稀疏图和边关系密切
负权 可以解决 不能解决 可以解决 可以解决

      Dijkstra算法最大的弊端是无法适负权边的图,但是其具有良好的扩展性,扩展后可以适应很多问题,另外用堆优化的Dijkstra算法的时间复杂度可以达到O(MlogN)。Floyd算法最然总体时间复杂度高,但是可以解决负权边,且均摊到每一点对上,在所有所算法中算比较好的。另外Floyd算法较小的编码复杂度也是其一大优势。当边有负权时,需要使用Bellman-Ford算法或者队列优化的Bellman-Ford算法。因此针对具体问题和实际需求做出选择还是很重要的。


      参考文献:《啊哈!算法》啊哈磊著

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/138529.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号