算法之几个常见的经典最短路径算法

1. Dijkstra算法

是解单源最短路径问题的贪心算法。

  • 有一向带权图 G =(V, E),包含右n个顶点,其中每条边的权是非负实数,定义数组 dist 为原点到G中各个顶点的距离,初始化为无穷大,
  • 维护一个顶点集合 S,初始时只包含源(即原点)
  • 每一步添加 v ∈ 𝑉 − S中具有到原点最小距离的顶点到 S 中,并更新dist数组
  • 经过n-1步,所有顶点都添加到S中,结束

更新数组的详情:

  • 假设当前S中只包含原点s,那么首先从s出发,第一次修改dis数组,此时,小于无穷大的代表和原点s直连(即连通),在dis数组中找到最小值对应的顶点v,加入到S中

  • 此时S中有{s,v}两个顶点,并且能保证的是dis数组中,s和v两个顶点的距离已经达到了最小值,因为选择v的时候,就是因为s到v的距离最短,反证:s到v如果不是最小,必然存在一条路径,s出发先经过非v的直连顶点,再可能经过其它顶点到s,显然,后者的距离不可能小于dis数组中的s到v的距离(也就是直连的距离)。所以能证明S中的顶点已经求出了最小值。

  • 当S中有{s,v}两个顶点时,可以第二次修改dis数组,修改的原则是:对于V-S中的顶点如w,第一次修改dis数组保留的是s和w直连的距离,当加入v后,s和w之间的距离就可以通过:计算出w到v的距离+v到s的距离,如果比之前dis数组中小,就更新,这一步的更新不能保证V-S中的顶点就已经求出了最短路径值。

  • 显然,上一步更新dis数组能求出w到s更短的路径,没有理由不去更新。此时,再考虑S-V中的顶点,在dis数组中,也就是寻找原点到自身和原点到之前加入的顶点s之外的值里面找最小值,假设为顶点v2到s的值。那么把v2加入到S中

  • 类似于2的分析,新加入的顶点v2到s的值此时已经达到了最小值。这个最小值可能是s-v-v2也有可能是s-v2。同样反证:假设s到v2之间不是前面说的那两种可能,而是s-u-…-v2,…表示没有或者其它的一个或多个顶点,显然,要先算出s-u的距离,s-u的距离,该怎么算呢?有以下几种可能,先回顾之前的v2怎么找到的呢?就是在dis数组中(除s和v)寻找最小值,此时的dis数组中顶点u对应的距离可能是s-u或者s-v-u,当然此时的s-u并未求出最小值,可以假设最终求出的最小值是s-w-…-u,要注意,只要经过非v和v2的点w(假设是直连于s),那么只是s-w的距离就大于s-v2,因为小于的话,选的就是w点加入S了,而不是顶点v2加入S;前面假设w直连s,如果非直连,总得先经过某个直连s的顶带q才能再连接其它顶点,那逻辑是一样的,s-q是大于s-v2的,不然就加入q了。至此可以说明s-v2确实已经找到了最小值。所以再次能证明S中的顶点已经求出了最小值。

  • 把v2加入S后,类似于3,第三次修改dis数组,然后寻找最小值,再加入,再寻找,循环这个步骤,到所有顶点都加入了S。同理,能证明S中的顶点确实都答到了最小值。

下面是个距离案例(这里盗用了上课讲的ppt )在这里插入图片描述在这里插入图片描述
再讨论以下复杂性分析:

  • 首先,复杂性分析需要依赖于特定的数据结构,不同的数据结构实现Dijkstra算法的复杂度是不同的。
  • 粗略的估计:用数组实现,最外层有n-1次的for循环,依赖于V的顶点数,假设为n,循环内部,更新一次dis数组,n的规模,这样就是O( n 2 n^2 n2)
  • 当然也可以用其它的数据结构实现,就是别的复杂度了,先不讨论。

上代码

	public final int MAX_ROUTE = Integer.MAX_VALUE;  // 代表无穷
    public int[] myDijstra(int[][] graph, int src){
        int n = graph.length;	// 顶点个数为n
        boolean[] used = new boolean[n];	// 是否加入S的标志位
        used[src] = true;
        int[] path = new int[n];	// 存储最短路径的数组
        System.arraycopy(graph[src],0,path,0,n);	// 第一次更新数组
        for (int i = 1; i < n; i++) {	// 需要n-1轮更新操作
            int tempMin = MAX_ROUTE;
            int tempIndex = -1;		// 记录即将加入S的顶点
             /* 寻找要加入的顶点 */
            for (int j = 0; j < n; j++) {
                if(!used[j] && path[j] < tempMin){
                    tempMin = path[j];
                    tempIndex = j;
                }
            }
            if(tempIndex == -1) break;	// 没找到到要加入的顶点,即所有距离都是无穷大,直接跳出循环
            used[tempIndex] = true;
            /* 松弛操作 */
            for (int j = 0; j < n; j++) {
                if( !used[j] && tempMin + graph[tempIndex][j] < path[j]){
                    path[j] = tempMin + graph[tempIndex][j];
                }
            }
        }
        return path;
    }

2. Floyd算法

和Dijkstra相比,直接算出的就是图中任意两点的最短距离,另外就是路径的负权值情况也可以计算,不过要求图中不能存在负环。它的算法思想是动态规划。比如:要求j-k的最短路径值,假设存在一个中间节点i,那么最小值就是对i进行遍历,求出每一个i对应的j-i的值+i-k的值,取其中的最小值。
实际实现的时候,可以这样理解:

  • dis[j][k]表示j-k的距离,初始化为图G的描述的二维数组graph[][]
  • 第一轮松弛,比如依赖第一个中间结点v(就是图的顶点),之前的dis数组描述的都是任意两结点直接相连的情况,现在加入第一个结点v,更改所有的dis[j][k],更新规则就是:dis[j][i] + dis[i][k] < dis[j][k]则更新
  • 第二轮松弛,比如再加入一个中间结点比如v2,之前的dis数组描述的是任意两结点直接相连或者经过中间结点v,此时,仍然执行上述的跟新规则,此时的k和v2对应,为了便于理解,无外乎此轮更新包括下面的情况:j-k,j-v-k,j-v2-k,j-v-v2-k,j-v2-v-k五种情况,第一轮松弛求出了j-i和j-v-i之间的较小值,第二轮松弛求的是dis[j][i] + dis[i][k]是否小于第一轮求出的dis[j][k],小于号前面包括:上一轮求出的j到k值和v2到k的值之和,j到v2无非是j-v2或者j-v-v2,v2到k无非是v2-k或者v2-v-k,也就是第二轮松弛求出了j-v2-k,j-v-v2-k,j-v2-v-k中的最小值,和第一轮松弛求出的j-k,j-v-k中最小值比较,取当中小值做为新的dis[j][k],也就是目前第二轮求出了任意两个结点j和k经过或不经过或经过部分v和v2的当前的最短路径。
  • 当松弛到第n轮,任意两个结点都算出了经过或者不经过或经过部分所有结点的最短路径。
  • 复杂度的计算:经过要有3次for循环,O( n 3 n^3 n3)

上代码

	public int[][] MyFloyd(int[][] graph){
        int n = graph.length;
        int[][] dis = new int[n][n];
        System.arraycopy(graph,0,dis,0,n);
        
        for(int i = 0; i < n; i ++){
            for (int j = 0; j < n; j++) {
                for (int k = 0; k < n; k++) {
                    if(dis[j][i] + dis[i][k] < dis[j][k]){
                        dis[j][k] = dis[j][i] + dis[i][k];
                        path[j][k] = i;
                    }
                }
            }
        }
        return dis;
    }

3. Bellman-Ford 算法

遵循一个比一个强大的规则,这个算法可以处理负权重的环,只不过存在负权重环的话,就可能没有最短路径存在了,无限转圈…

Floyd算法和Dijkstra算法的思考角度差不多,都是从顶点的角度出发去松弛边,前者比后者充满了暴力般的简洁。而Bellman-Ford 并不是纯粹从顶点出发的思路,而是外层循环顶点,内层循环边,和Dijkstra相似的是,也是处理从单个顶点出发求到其它所有顶点的最短距离,具体思路如下:

  • 首先,定义一个dis数组,表示原点到其它结点的距离,初始化为无穷大,
  • 先考虑一轮循环,加入第一条边w(u,v),很明显,除非u或者v中有一个是原点,才能更新dis数组
  • 再加入一条边,考虑一种特殊情况,第一次加入的u是原点,这样,更新了原点到v的距离,这次的加入是w(v,v2),那么很明显,v2也能更新了,需要w(u,v)+w(v,v2),但是能直接更新吗?先需要和当前值比以下,取较小值才是合理的,当然只有两条边的情况下是和无穷在比较
  • 再加入第三条边,加入碰巧是w(u,v2),显然需要和第二步的值做比较才能决定更新
  • 不断的加入新的边w(x,y),统一的计算dis[x] + w(x,y) < dis[y]吗?小于则更新dis[y]
  • 全部的边都加入了,是不是求出了原点到其它结点的最短距离呢?有可能。分析第一轮循环做了哪些,加入一条边w(x,y),就算一下从原点到y的距离是不是先经过x再到y会更短,但是值得注意,上面的特殊情况是先加入和原点相邻的边,再加入和原点相邻的边的相邻的边,如果相反呢,那么判断原点到相邻的边距离是无穷大,明显就不会更新了,也就没有路径中存在两个边的情况了。
  • 所以需要再更新,之前的更新dis假设保留了只经过一条边结果,这一轮更新,便能把经过2条边的情况包含进去,因为每轮更新都只能再上一轮更新的基础上再加一条边
  • 所以考虑极端情况,最短路径包含n-1条边,那么就需要n-1轮更新
  • 另外,如果某一轮更新后,dis值没有变化,那么更新就可以停止,如果n-1轮更新完毕后dis还有变化,说明存在负环
  • 复杂度的话明显是顶点个数*边的个数

上代码

    public int[] MyBellmanFord(int[][] graph){
        int n = graph.length;
        int m = 0;
        int[] path = new int[n];
        Arrays.fill(path,MAX_ROUTE);
        List<List<Integer>> route = new ArrayList<>();
        /* 便于后面取边,不是核心代码 */
        for (int i = 0; i < n; i++) {
            for(int j = 0; j < n; j ++){
                if(graph[i][j] != MAX_ROUTE && graph[i][j] != 0) {
                    route.add(new ArrayList<Integer>(Arrays.asList(i,j,graph[i][j])));
                    m++;
                }
            }
        }
        path[0] = 0;	// 初始化原点到原点为0
        int[] watch = new int[n];	// 判断提前终止
   		boolean negCircle = false;	// 负环标志位
   		/* 核心算法 */
        for (int i = 0; i < n - 1; i++) {
            System.arraycopy(path,0,watch,0,n);
            for (int j = 0; j < m; j++) {
                List<Integer> list = route.get(j);
                if(path[list.get(0)] + list.get(2) < path[list.get(1)]){
                    path[list.get(1)] = path[list.get(0)] + list.get(2);
                }
            }
            // 判断是否可以停止更新
            for (int k = 0; k < n; k++) {
                if(path[k] != watch[k]) {
                    break;
                }
                if(k == n - 1)   negCircle = true;
            }
            if(negCircle)   break;

        }
		/* 判断负环的存在 */
        for (int i = 0; i < n - 1; i++) {
            for (int j = 0; j < m; j++) {
                List<Integer> list = route.get(j);
                if(path[list.get(0)] + list.get(2) < path[list.get(1)]){
                    negCircle = true;
                }
            }
        }
        return path;
    }
  • 2
    点赞
  • 47
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值