Java高阶数据结构 图的最短路径问题

news2025/1/9 1:18:29

图的最短路径问题!

在这里插入图片描述

文章目录

  • Java高阶数据结构 & 图的最短路径问题
    • 1. Dijkstra算法【单源最短路径】
      • 1.1 Dijkstra算法证明
      • 1.2 Dijkstra算法代码实现
      • 1.3 堆优化的Dijkstra算法
      • 1.4 堆优化Dijkstra算法代码实现
    • 2. Bellman-Ford算法【单源最短路径】
      • 2.1 BF算法证明
      • 2.2 BF算法代码实现
      • 2.3 队列优化的BF算法:SPFA算法
      • 2.4 SPFA算法代码实现
      • 2.5 复杂度分析
    • 3. Floyd-Warshall算法【多源最短路径】
      • 3.1 算法思想
      • 3.2 代码实现

Java高阶数据结构 & 图的最短路径问题

图的基础知识博客:传送门

最短路径问题: 从在带权图的某一顶点出发,找出一条通往另一顶点的最短路径,最短也就是沿路径各边的权值总 和达到最小

一共会讲解三种算法

  1. Dijkstra算法【单源最短路径】
  2. Bellman-Ford算法【单源最短路径】
    • 改进:SPFA算法
  3. Floyd-Warshall算法【多源最短路径】

单源最短路径问题: 给定一个图G = ( V , E ) G=(V,E)G=(V,E),求源结点s ∈ V s∈Vs∈V到图中每个结点v ∈ V v∈Vv∈V的最短路径。

多源最短路径问题: 就是找到每个顶点到除本身的顶点的最短路径

两顶点不连通,则不存在最短路径–>∞

在后面的讲解中,就不要联想到邻接矩阵了,这样脑子CPU都要烧烂了,代码实现需要邻接矩阵,而不是图就长成矩阵这么抽象的样子。

  • 看原图去分析思考就行了,记住边的代码表达即可
    • 比如说,一个顶点连出去的边,在原图中很明显,在邻接矩阵中不明显,但是后续我们要得到连出去的边,我们也有方法呀,所以在算法思考的时候,先不要在意这点!
    1. 算法思路:原图
    2. 代码实现:根据算法翻译即可

带负权的图,可能可以找到最短路径,也可能找不到

  1. 带负权回路的图,不存在最短路径

判断方法:

  • 一条路径上通过两次一样的顶点,第二次反而更短了,则必然存在一个 负权回路
  • 即,这个环的权值和为负数

在这里插入图片描述

则说明:一张图的最短路径,一定满足边数<= n-1

原因:

因为可以通过这个负权回路,导致一些最短路径趋近于负无穷大

  • 即,在这个环里,无限循环

(带负权一定要是有向图,无向图的话,负权边一定构成两个顶点的负权回路)

在这里插入图片描述


以下算法代码实现简单,重点看算法思想!

1. Dijkstra算法【单源最短路径】

  • 思路适用于解决带权重的有向图上的单源最短路径问题
    • 无向图当然也可以~
  • 但是,算法要求图中的所有边的权重都是非负的~
    • 再讲解完算法后你就知道为什么了

定义:(不懂没关系,最后我有详细的证明)

  1. 定义一个dist数组,这个数组记录出发点到其他点的最短路径

    • 一开始没找出来的时候,默认全为无穷大,到自身距离为0
  2. 定义一个path数组,代表每个顶点的前一个节点

    • 通过这个数组可以还原路径
  3. 定义一个操作:“松弛”

    • 松弛一个顶点,就是将其连通的所有边进行以下操作:

    • 设此顶点为i,连通终点为j

    • 判断:dist[i] + weight(i, j) < dist[j]

    1. 如果为真,则说明j此时的最短路径不是最短路径,path[j] = i,dist[j] = dist[i] + weight(i, j)
    2. 如果为假,什么也说明不了

步骤:

  1. 选取此时dist中的最小元素下标,标记它,断言此顶点为最短路径
    • (值为0的出发点此时必然第一个被标记和松弛)
  2. 松弛刚才被标记的顶点
  3. 选取松弛完dist中最小元素下标,标记它,断言此顶点为最短路径
  4. 松弛刚才被标记的顶点
  5. 选取松弛完dist中最小元素下标,标记它,断言此顶点为最短路径
  6. 松弛刚才被标记的顶点
  7. 直到所有顶点都被标记

在这里插入图片描述

可以有两种理解方式:

  1. 标记 松弛 标记 松弛 标记 松弛 … 标记(最后一次没必要松弛)
    • 标记最短路径顶点,松弛这个顶点
  2. (标记出发点后) 松弛 标记 松弛 标记 松弛 标记 … 松弛 标记
    • 松弛后诞生一个最短路径,标记

获得最短路径:

  • 通过path数组,不断向出发点方向“跳”,直到到达出发点

例子:

在这里插入图片描述

  • 如以上连通无向图,求0为出发点,求0到其余点的最短路径

动图演示:

在这里插入图片描述

来源:【算法】最短路径查找—Dijkstra算法_哔哩哔哩_bilibili

  • 讲的很好!
  • 但是没有证明为什么,接下来就来看看为什么吧

1.1 Dijkstra算法证明


  1. 松弛操作的作用
  • 所以,松弛操作,保证了目前看来(再下一次松弛之前),顶点们在dist数组内的中是当前能达到的最小值

  • 并且改变j的路径为,【0 , i】延伸一条边【i , j】
    在这里插入图片描述
    一样短会怎么样呢?

  • 不会怎么样,只是说明最短路径不唯一

至少对这个顶点后续的延伸是没区别的,因为0到j的距离都一样,后续该怎么延伸出去还是怎么延伸出去

在这里插入图片描述


  1. 为什么可以断言这个顶点一定是最短路径?

重要原因:图没有负权

我们按照算法思路先走一走

  1. 选择0,这是显然的,松弛0后,诞生了“第一代最短路径”(路径上只有1条边)
  2. 标记松弛后的dist数组(未标记顶点)中最小的顶点

那么我们就断言第一代最短路径中最短的那条路径,就是“正确的”

  • 这也是携带了为什么Dijkstra只能解决带非负权图的原因
  • 就是因为,它的算法的前提,就是没有负权

第一代最短路径最短那那条L,就不可能通过任何方式让其更短

在这里插入图片描述

  • 因为这个点的其他路径就只能是第二代,或者更多
  • 而这条路径,是通过第一代最短路径的另一些边的,而这些边本身就比L大,并且此后路径的边都是正的,必然比L大
  1. 松弛刚才标记的顶点1,诞生“第二代最短路径”(路径上为2条边)
  2. 标记松弛后的dist数组(未标记顶点)中最小的顶点

那么我们就断言第一代最短路径中最短的那条路径L2,就是“正确的”

同样的道理,这条路径要么是一条边的,要么是两条边的

在这里插入图片描述

刚才为什么不一起选择7这个顶点呢?

  • 【0到1】 比 【0到7】 短,所以可能【0到1】在到其他顶点,再回到7这个顶点

在第二代最短路径中,松弛操作保证了路径为一条边的和路径为两条边的顶点在dist值最小

  • 即,局部范围内,他们是最短路径(在现在能触及的范围内,他们的dist值是正确的)
  • 即,以出发点为标准,最多延伸两条边的子图范围内,他们都是最短路径
    • 这个子图不包含所有的“两条边的路径”,不包含的部分也不需要出现,因为没有负权,包含在内的“两条边的路径”,是由上一次的最短路径顶点延伸出来的,那么这个条包含在子图内的“两条边的路径”,一定是比不包含的要短~

在这里插入图片描述

这一次,最短的是【0到7】,同样的原因,可以断言7此时是正确的最短路径

  • 刚才的证明了此时7是这个范围内的最短路径,这就够了
  • 后续不会再有到7路径更短的顶点了(别的路径只能增加)
  1. 松弛顶点7,诞生第三代最短路径
  2. 标记松弛后dist值(未标记顶点中)最小的顶点

同样的,以出发点为标准,最多延伸三条边的子图(<=3,一样的,不一定包含所有的“三边路径”和“两边路径”,一定包含“更有权威的”路径)范围内,他们都是最短路径

在这里插入图片描述

  • 选择顶点6(此时顶点6)~
    • 因为后续到这顶点就只能增了~

在这里插入图片描述

依照这个思路下去,所有顶点被标记,结束!

  • 这个例子中,边数最多的路径,为“四边路径”,【0到4】
    在这里插入图片描述

  1. 为什么可以通过path确认最短路径?

以【0到4】为例子

【0到4】=【0到5】+【5到4】

  1. 其实一条长路径一定是由短路径拼接起来的(由刚才的算法得出结论,最短路径的更新,是在前一个顶点的最短路径基础上延伸一条边)
  2. 在这里插入图片描述

所以,一个最短路径的子路径为别的顶点的最短路径,所以可以通过下标的往回“跳”,得到真实路径

证明完毕~

1.2 Dijkstra算法代码实现

  • 代码实现看起来很简单,但是原理是刚才那样的复杂
	/**
     *
     * @param src 出发点
     * @param dist 要求把最短路径长存放在这个数组里
     * @param path 要求将前面点存放在这个数组里
     */
public void dijkstra(char src, int[] dist, int[] path) {
    //获取顶点下标
    int srcIndex = getIndexOfV(src);

    //初始化dist
    Arrays.fill(dist, Integer.MAX_VALUE);
    dist[srcIndex] = 0;//起始点

    //初始化path
    Arrays.fill(path, -1);
    path[srcIndex] = srcIndex;//如果是前一个顶点是本身的话,则说明到达起始点

    //定义visited数组
    int n = arrayV.length;
    boolean[] visited = new boolean[n];

    //开始标记与松弛操作了
    //由于我们知道每次循环都会标记一个,那么循环次数就知道了,所以我们就用特定的for循环去写
    for (int i = 0; i < n; i++) {

        //找dist最小值
        int index = srcIndex;//这个无所谓
        int min = Integer.MAX_VALUE;
        for (int j = 0; j < n; j++) {
            if(!visited[j] && dist[j] < min) {
                index = j;
                min = dist[j];
            }
        }
        //标记
        visited[index] = true;

        //松弛
        for (int j = 0; j < n; j++) {
            //被必要松弛到标记的顶点的,因为没用(再之前的证明中),你要也可以
            if(!visited[j] && matrix[index][j] != Integer.MAX_VALUE
               && dist[index] + matrix[index][j] < dist[j]) {
                //松弛导致的更新操作,更新其路径为【0,index】延伸一条边【index,j】
                dist[j] = dist[index] + matrix[index][j];
                path[j] = index;
            }
        }
    }
}

测试:

打印路径和路径长的方法:

public void printShortPath(char vSrc,int[] dist,int[] pPath) {
    //1. 获取顶点下标
    int srcIndex = getIndexOfV(vSrc);
    int n = arrayV.length;
    //2、遍历pPath数组 的n个 值,
    // 每个值到起点S的 路径都打印一遍
    for (int i = 0; i < n; i++) {
        //自己到自己的路径不打印
        if(i != srcIndex) {
            ArrayList<Integer> path = new ArrayList<>();
            int parentI = i;
            while (parentI != srcIndex) {
                path.add(parentI);
                parentI = pPath[parentI];
            }
            path.add(srcIndex);
            //翻转path当中的路径
            Collections.reverse(path);
            for (int pos : path) {
                System.out.print(arrayV[pos] +" -> ");
            }
            System.out.println(dist[i]);
        }
    }
}

测试案例:

在这里插入图片描述

public static void testGraphDijkstra() {
    String str = "syztx";
    char[] array = str.toCharArray();
    GraphByMatrix g = new GraphByMatrix(str.length(),true);
    g.initArrayV(array);
    g.addEdge('s', 't', 10);
    g.addEdge('s', 'y', 5);
    g.addEdge('y', 't', 3);
    g.addEdge('y', 'x', 9);
    g.addEdge('y', 'z', 2);
    g.addEdge('z', 's', 7);
    g.addEdge('z', 'x', 6);
    g.addEdge('t', 'y', 2);
    g.addEdge('t', 'x', 1);
    g.addEdge('x', 'z', 4);
/*    
	搞不定负权值
    String str = "sytx";
    char[] array = str.toCharArray();
    GraphByMatrix g = new GraphByMatrix(str.length(),true);
    g.initArrayV(array);
    g.addEdge('s', 't', 10);
    g.addEdge('s', 'y', 5);
    g.addEdge('t', 'y', -7);
    g.addEdge('y', 'x', 3);
*/

    int[] dist = new int[array.length];
    int[] parentPath = new int[array.length];
    g.dijkstra('s', dist, parentPath);
    g.printShortPath('s', dist, parentPath);
}
public static void main(String[] args) {
    testGraphDijkstra();
}

在这里插入图片描述

1.3 堆优化的Dijkstra算法

  • 时间复杂度为O(N2)
    • 标记N次,每次都要遍历数组
    • 但是这个原始的算法,适合解决稠密图的最短路径~

img

  • 但如果是稀疏图的话,每次都遍历一次数组,这个复杂度太大了
  • 所以有了以下堆优化的算法

本质原理一样:

定义存放在堆里面的类:

class Point {
    int index;
    int minPath;
}
  1. 每次松弛都向堆里面放这个对象(而不是改变堆里面对应index的值)

    • 堆是如何实现与dist数组一样“更新”的呢?
    • 因为我新加入的这个对象,会比堆原本的那个index值的那个值要小,肯定会在其之前被取出
  2. 取堆顶元素

    • 如果这个元素被标记过,达咩,不要(continue)

    • 如果这个元素没有被标记过,哟西,标记它,并且对其进行松弛操作

      • 标记后,其后面出现遗留的点也无所谓咯

而优化后是适合稀疏图的,因为松弛入堆操作的次数可以认为是C常数,那么复杂度为O(N*log2N)

  • 但是如果是稠密图,这个松弛入堆操作的次数则会接近于N,算法复杂度到达O(N2*log2N)
  • 比不优化的还差

1.4 堆优化Dijkstra算法代码实现

定义Point类:

static class Point {
    int indexV;
    int distValue;

    public Point(int indexV, int distValue) {
        this.indexV = indexV;
        this.distValue = distValue;
    }
}

核心方法:

  • 根据的就是刚才的算法!
/**
 *
 * @param src 出发点
 * @param dist 要求把最短路径长存放在这个数组里
 * @param path 要求将前面点存放在这个数组里
 */
public void pQDijkstra(char src,int[] dist,int[] path) {
    //获得顶点下标
    int srcIndex = getIndexOfV(src);
    //初始化dist
    Arrays.fill(dist, Integer.MAX_VALUE);
    dist[srcIndex] = 0;//起始点

    //初始化path
    Arrays.fill(path, -1);
    path[srcIndex] = srcIndex;//如果是前一个顶点是本身的话,则说明到达起始点

    //定义visited数组
    int n = arrayV.length;
    boolean[] visited = new boolean[n];

    //定义小根堆
    PriorityQueue<Point> queue = new PriorityQueue<Point>(
            (o1, o2) -> {
                return o1.distValue - o2.distValue;
            }
    );

    queue.offer(new Point(srcIndex, 0));
    while(!queue.isEmpty()) {
        Point point = queue.poll();
        int index = point.indexV;
        //被标记过,达咩!
        if(visited[index]) {
            continue;
        }
        //标记
        visited[index] = true;
        //松弛
        for (int j = 0; j < n; j++) {
            //被必要松弛到标记的顶点的,因为没用(再之前的证明中),你要也可以
            if(!visited[j] && matrix[index][j] != Integer.MAX_VALUE
                    && dist[index] + matrix[index][j] < dist[j]) {
                //松弛导致的更新操作,更新其路径为【0,index】延伸一条边【index,j】
                dist[j] = dist[index] + matrix[index][j];
                path[j] = index;
                queue.offer(new Point(j, dist[j]));
            }
        }
    }
}

测试:

g.pQDijkstra('s', dist, parentPath);
g.printShortPath('s', dist, parentPath);
public static void main(String[] args) {
    testGraphDijkstra();
}
  • 跟刚才一样的案例

结果一致:

在这里插入图片描述

2. Bellman-Ford算法【单源最短路径】

如果把Dijkstra算法称为深度优先,那么Bellman-Ford算法就是广度优先,也更加直接与粗暴

  • 简称BF(更暴力算法BF还真对上了O(∩_∩)O哈哈~)

与Dijkstra算法不同,其可以解决带负权的图的最短路径问题!

用到Dijkstra用过的操作:

在这里插入图片描述

不同的是它的算法步骤:

  1. 对全局所有的顶点,都进行一次松弛操作
  2. 这里不做任何标记,因为现在直接相连的点,最终如果有负权,还是有可能通过“边数更长的最短路径”到达该点
  3. 这样的操作进行N - 1轮,N为顶点的个数
    • 如果这一次没有做任何更新,则以后的松弛也不会有任何的更新,退出循环,结束!

获得最短路径:

  • 通过path数组,不断向出发点方向“跳”,直到到达出发点

例子:

在这里插入图片描述

动图演示:](https://img-blog.csdnimg.cn/4d3036a6195f49dc8e9fc098628e2987.gif)

来源:【熊羊一锅鲜】Ep.13 单源最短路径Bellman-Ford算法及SPFA算法_哔哩哔哩_bilibili

2.1 BF算法证明

一些点在Dijkstra的证明那已经讲过了

在这里插入图片描述

  • 不同的一点在于,BF算法松弛操作的顶点,每一次都是所有顶点~

你也会发现,一开始如果先松弛的不是出发点,或者是“已经有路径的顶点”,这个松弛操作是没有意义的,因为这个顶点的dist值为∞

  • 这也衍生出了一个问题:顶点松弛的先后会不会有影响
  1. 第N代最短路径中“边最长为N”的子图范围内,各个顶点的dist值在这个局部范围内【限制路径最多有N条边】是正确的,是最短的

这一点,是 通过松弛操作来保证 的,原本Dijkstra算法没有保证这个子图的完整性,而BF算法由于每次都是松弛所有顶点,所以这个子图是完整的~

如第一代子图(第一次循环的结果):

在这里插入图片描述

如第二代子图(第二次循环的结果):

在这里插入图片描述

如第三代子图(第三次循环的结果):

你可能有一个错觉:这不是已经涉及所有顶点了吗,那么这就是在全局范围内的正确?

否,因为这里限制路径长最大是“三条边”,在这个限制下是正确的

  • 例如【0到6】最短为三步,再走一步到7确实是11<12但是,这就是四步了呀

在这里插入图片描述

如第四代子图(第四次循环的结果):

在这里插入图片描述

到了第五代子图的时候(第五次循环的结果):

在这里插入图片描述

  • 所有顶点都没被更新~
  • 跳出循环,算法结束
  • 说明了里面“最长的最短路径”是四条边的
  1. 顶点松弛的先后会不会有影响

其实 被松弛涉及到的顶点i有更新的条件 是:顶点j(顶点j松弛后涉及到了顶点i)更新过

而你也发现了这个算法产生了大量的没用操作

  1. 如果先对“后面”的顶点松弛,可能没有作用
  2. 假设此次是第n次循环,那么一个顶点目前最短路径边数小于等于n-2的顶点
    • 例如第二次循环的时候,出发点没必要松弛
    • 第三次循环的时候,最短路径边数为1的顶点没有必要松弛
      • 因为松弛完后最多为两条边,而两条边的时候在第二代子图(第二次循环结果)中,已经是得到最短的了
      • 所以没有必要!

在这里插入图片描述

你可能会觉得,先松弛出发点,那么可能“后面的顶点”也会链式的被更新到

  • 但这样,“前面的路径”发生改变,这个“后面的顶点”也有改变的风险

而我们只需要保证这一个严格成立即可

第N代最短路径中“边最长为N”的子图范围内,各个顶点的dist值在这个局部范围内【限制路径最多有N条边】是正确的,是最短的

最坏的情况下,就是完全“逆行”,即使这样,每一代都能够满足这一点

  • 因为每个顶点都要松弛
  • 顺序不同只是改变其“连锁反应”【就是因为刚才它刚变了,导致我虽然和它都是第n此松弛,但是我却因此可以更新别的顶点】
  • 不会改变其“必然的变化”
    • 这必然的变化,不是由于连锁反应产生的

动图分析:(抽象)

  • 希望你能get到

在这里插入图片描述

  1. 最后一次循环的更新后,难道不应该再次松弛去更新其他顶点吗?

答:不用,理由就是到达第N - 1次循环,结果是第N-1代子图,这已经到达了“全局范围”,所有顶点的dist值最短路径都是正确的。

  1. 所有的路径本身就小于等于N - 1,在最后一次循环更新的顶点,则说明其路径达到最长值:n-1条边

  2. 松弛的作用结果是延伸刚才的路径,那么刚才的路径已经是边数最大,松弛不会成功,没有必要继续松弛了

  3. 如果是带负权回路的,继续松弛一直都会更新

    • 会产生第∞代子图
    • 需要循环后去判断~

证明完毕~

2.2 BF算法代码实现

//这也是判断是否有负权回路的算法
public boolean bellmanFord(char src,int[] dist,int[] path) {
    //获得顶点下标
    int srcIndex = getIndexOfV(src);
    //初始化dist
    Arrays.fill(dist, Integer.MAX_VALUE);
    dist[srcIndex] = 0;//起始点

    //初始化path
    Arrays.fill(path, -1);
    path[srcIndex] = srcIndex;//如果是前一个顶点是本身的话,则说明到达起始点

    int n = arrayV.length;

    //循环n-1次,每次松弛一个顶点
    for (int i = 0; i < n - 1; i++) {
        for (int j = 0; j < n; j++) {
            for (int k = 0; k < n; k++) {
  				if(matrix[j][k] != Integer.MAX_VALUE && 
                            matrix[j][k] + dist[j] < dist[k]) {
                        dist[k] = matrix[j][k] + dist[j];
                        path[k] = j;
                }
            }
        }
    }
    //检测负权回路
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            //至于一样短的情况下,无所谓,这就是最短路径不唯一呗~
            //
            if (matrix[i][j] != Integer.MAX_VALUE 
                && matrix[i][j] + dist[i] < dist[j]) {
                return false; //false代表了有负权回路

            }
        }
    }
    return true;
}

测试案例:

在这里插入图片描述

public static void testGraphBellmanFord() {
    String str = "syztx";
    char[] array = str.toCharArray();
    GraphByMatrix g = new GraphByMatrix(str.length(),true);
    g.initArrayV(array);
    g.addEdge('s', 't', 6);
    g.addEdge('s', 'y', 7);
    g.addEdge('y', 'z', 9);
    g.addEdge('y', 'x', -3);
    g.addEdge('z', 's', 2);
    g.addEdge('z', 'x', 7);
    g.addEdge('t', 'x', 5);
    g.addEdge('t', 'y', 8);
    g.addEdge('t', 'z', -4);
    g.addEdge('x', 't', -2);
    //负权回路实例
    //        g.addEdge('s', 't', 6);
    //        g.addEdge('s', 'y', 7);
    //        g.addEdge('y', 'z', 9);
    //        g.addEdge('y', 'x', -3);
    //        g.addEdge('y', 's', 1);
    //        g.addEdge('z', 's', 2);
    //        g.addEdge('z', 'x', 7);
    //        g.addEdge('t', 'x', 5);
    //        g.addEdge('t', 'y', -8);
    //        g.addEdge('t', 'z', -4);
    //        g.addEdge('x', 't', -2);
    int[] dist = new int[array.length];
    int[] parentPath = new int[array.length];
    boolean flg = g.bellmanFord('s', dist, parentPath);
    if(flg) {
        g.printShortPath('s', dist, parentPath);
    }else {
        System.out.println("存在负权回路");
    }
}
public static void main(String[] args) {
    testGraphDijkstra();
}

测试一个不带负权回路的案例:

在这里插入图片描述

测试带负权回路的:
在这里插入图片描述

2.3 队列优化的BF算法:SPFA算法

刚才提到:

在这里插入图片描述

那么我们只需要松弛更新了的顶点就好了呀,

结合BF的视角:

  1. 每一次循环更新的顶点,设他们的集合为set1
  2. 下一次循环更新的顶点,设他们的集合为set2

要想满足BF算法的思想:

  • set1的整体要在set2的整体前松弛完才行
  • 才能有第一代子图—第二代子图—第三代子图这样的效果

所以用到数据结构:队列


步骤就是:

  1. 将起始点放入队列
  2. 循环以下操作
    • 取队头得到一个顶点
    • 松弛这个顶点
  3. 直到队列为空,即不再有元素更新,结束算法

显然,这个算法没法判断负权回路的存在,会死循环下去~

2.4 SPFA算法代码实现

public void queueBellmanFord(char src,int[] dist,int[] path) {
    //获得顶点下标
    int srcIndex = getIndexOfV(src);
    //初始化dist
    Arrays.fill(dist, Integer.MAX_VALUE);
    dist[srcIndex] = 0;//起始点

    //初始化path
    Arrays.fill(path, -1);
    path[srcIndex] = srcIndex;//如果是前一个顶点是本身的话,则说明到达起始点

    int n = arrayV.length;

    //定义一个队列
    Queue<Integer> queue = new LinkedList<>();
    queue.offer(srcIndex);

    //开始循环松弛
    while(!queue.isEmpty()) {
        int top = queue.poll();
        for (int i = 0; i < n; i++) {
            if (matrix[top][i] != Integer.MAX_VALUE
                    && matrix[top][i] + dist[top] < dist[i]) {
                dist[i] = matrix[top][i] + dist[top];
                path[i] = top;
                queue.offer(i);
            }
        }
    }
}

测试:
在这里插入图片描述

  • 带负权回路的案例会死循环,这里就不展示了~
public static void testGraphBellmanFord() {
    String str = "syztx";
    char[] array = str.toCharArray();
    GraphByMatrix g = new GraphByMatrix(str.length(),true);
    g.initArrayV(array);
    g.addEdge('s', 't', 6);
    g.addEdge('s', 'y', 7);
    g.addEdge('y', 'z', 9);
    g.addEdge('y', 'x', -3);
    g.addEdge('z', 's', 2);
    g.addEdge('z', 'x', 7);
    g.addEdge('t', 'x', 5);
    g.addEdge('t', 'y', 8);
    g.addEdge('t', 'z', -4);
    g.addEdge('x', 't', -2);
    int[] dist = new int[array.length];
    int[] parentPath = new int[array.length];
    g.queueBellmanFord('s', dist, parentPath);
    g.printShortPath('s', dist, parentPath);
}
public static void main(String[] args) {
    testGraphDijkstra();
}

在这里插入图片描述

2.5 复杂度分析

M是边的数量,N是顶点的个数

则BF算法的时间复杂度为O(N * M),而大部分情况下SPFA算法的时间复杂度为O(M),最坏情况是O(N * M)

如果是稠密图的话,M会变得很大很大,两个算法的时间复杂度都会变得很大!

  • 所以这两种算法适合去解决带负权的稀疏图~

3. Floyd-Warshall算法【多源最短路径】

3.1 算法思想

在这里插入图片描述

在这里插入图片描述

但是,值得注意的是:根据相邻顶点之间的权值,要记录下来每个顶点“一条边的路径”!否则这个算法没有用武之地

  • 因为dist初始都是无穷大,并且这个算法的步骤没有用到matrix数组,即没有用到权值

并且:最外层循环的循环遍历的应该对应的是中间节点

如果外两层是i,j循环(路线【i到j】),然后最内层循环是k循环(中间节点),这样子是很局限的,因为这i到j再k循环结束后,就被确定了唯一的最短路径,而不是在单单这一次就确定了,这是很不合理的。

  • 因为在后面的变化中,i到j的路线,是很有可能被改变的,而这种写法,后续是没法改掉的!

正确的应该是最外层是k循环(中间节点),内两层是i,j循环(路线【i到j】),这样才能保证路径一定能被发现,并且后续【i到j】的路径也能会应变,直到全部循环结束而被确立下来~

定义:

在这里插入图片描述

左图为:一个连通图

右图为:它的dist二维数组(初始状态,未开始算法)

  • 定义一个dist二维数组,(i, j)代表这i到j的最短路径路径长
  • 定义一个path二维数组,(i, j)代表这个i到j的路径的上一个顶点
    • 在这一行上跳动即可
    • 为什么可以跳动,原因与以上一致

可见时间复杂度为:O(N3)

推荐:Ep.23 弗洛伊德Floyd-Warshall算法_哔哩哔哩_bilibili

3.2 代码实现

public void floydWarShall(int[][] dist, int[][] path) {
    //初始化dist和path
    int n = arrayV.length;
    for (int i = 0; i < n; i++) {
        Arrays.fill(dist[i], Integer.MAX_VALUE);
        Arrays.fill(path[i], -1);
    }

    //每一个顶点的第一代子图,记录在dist和path中,现在局部的最短路径
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            if(matrix[i][j] != Integer.MAX_VALUE) {
                dist[i][j] = matrix[i][j];
                path[i][j] = i;//这一行的上一个就是i
            }else {
                path[i][j] = -1;//不存在路径
            }
            if(i == j) {
                dist[i][j] = 0;
                path[i][j] = j;//跳回本身
            }
        }
    }
    //进行算法,每个顶点都当一回中介点
    //每个顶点都被当做一次起始点,终点
    //一个点即使起始点有时中介点又是终点,好像也无所谓
    //只要满足那个方程!
    //顺序完全没关系~
    for (int k = 0; k < n; k++) {
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                //规定k代表的是中介点,i为起始点,j为终点
                boolean flag = dist[i][k] != Integer.MAX_VALUE
                        && dist[k][j] != Integer.MAX_VALUE
                        && dist[i][k] + dist[k][j] < dist[i][j];
                //取不取等无所谓,只是不同最短路径的区别罢了
                if (flag) {
                    dist[i][j] = dist[i][k] + dist[k][j];
                    path[i][j] = path[k][j];//【i,j】以【k,j】为子路径
                }

            }
        }
    }
}
  • i与j与k相等没有什么大碍

测试:

在这里插入图片描述

public static void testGraphFloydWarShall() {
    String str = "12345";
    char[] array = str.toCharArray();
    GraphByMatrix g = new GraphByMatrix(str.length(),true);
    g.initArrayV(array);
    g.addEdge('1', '2', 3);
    g.addEdge('1', '3', 8);
    g.addEdge('1', '5', -4);
    g.addEdge('2', '4', 1);
    g.addEdge('2', '5', 7);
    g.addEdge('3', '2', 4);
    g.addEdge('4', '1', 2);
    g.addEdge('4', '3', -5);
    g.addEdge('5', '4', 6);
    int[][] dist = new int[array.length][array.length];
    int[][] path = new int[array.length][array.length];
    g.floydWarShall(dist,path);
    for (int i = 0; i < array.length; i++) {
        g.printShortPath(array[i],dist[i],path[i]);
        //把一行一行传过去~
        //一行代表一个顶点到其他顶点的最短路径
    }
}
public static void main(String[] args) {
    testGraphFloydWarShall();
}

在这里插入图片描述


文章到此结束!谢谢观看
可以叫我 小马,我可能写的不好或者有错误,但是一起加油鸭🦆

这就是最短路径问题的全部内容了,如果有什么不懂可以留言/私信讨论!


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/515281.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

常见的基础模块电路,你都能看懂吗?

文章开始前&#xff0c;先来考考大家~ 下面的五副电路图&#xff0c;你能看懂几个&#xff1f; 目录 01.电源电路 02.运算放大器电路 03.信号产生电路 04.信号处理电路 05.传感器及其应用电路 06.显示电路 TDA2030电路图 34063电路图 555电路 TDA2030电路图 三极管分立元…

音视频八股文(11)-- ffmpeg 音频重采样

1重采样 1.1 什么是重采样 所谓的重采样&#xff0c;就是改变⾳频的采样率、sample format、声道数等参数&#xff0c;使之按照我们期望的参数输出。 1.2 为什么要重采样 为什么要重采样&#xff1f;当然是原有的⾳频参数不满⾜我们的需求&#xff0c;⽐如在FFmpeg解码⾳频…

从头开始学习Python接口自动化测试:编写测试用例,执行测试以及生成测试报告

Python接口自动化测试详解 随着Web服务和移动应用不断增多&#xff0c;以及对API和微服务的需求不断增加&#xff0c;API已成为现代应用程序中必不可少的组件。自动化测试框架可以大大简化API测试的过程&#xff0c;并确保其正确性和稳定性。Python是一种非常流行的编程语言&a…

洛谷B2100 同行列对角线的格

同行列对角线的格 题目描述 输入三个自然数 N N N&#xff0c; i i i&#xff0c; j j j&#xff08; 1 ≤ i ≤ n 1 \le i \le n 1≤i≤n&#xff0c; 1 ≤ j ≤ n 1 \le j \le n 1≤j≤n&#xff09;&#xff0c;输出在一个 N N N \times N NN 格的棋盘中&#xff08;行…

西门子1200PLC如何在威纶通HMI上进行时间显示

先生成定时器DB&#xff0c;然后在引脚绑定变量&#xff0c;在西门子PLC中&#xff0c;DINT和TIME之间可以隐含转化。 第一种方法&#xff1a;触摸屏元件设置成DINT类型 数值元件资料格式为32-bit Signed&#xff0c;对应PLC中即为DINT类型。小数点以下没有位数。这是我们测试…

Android RecyclerView实现侧滑删除,附 Demo

距上次写博客有半年多了&#xff0c;回忆起来都觉得不可思议&#xff0c;中间也想憋俩大招&#xff0c;总是被耽误&#xff0c;这俩月忙完之后&#xff0c;终于空下来了&#xff0c;恰好新项目我和UI俩人商量一下&#xff0c;用MD来实现app。中间有个需求是RecyclerView中侧滑显…

ch06-Pytorch的正则化与归一化

ch06-Pytorch的正则化与归一化 0.引言1.weight decay 和 dropout1.1.Regularization1.2.Dropout 2.Normalization2.1.Batch Normalization2.2.Batch Normalization in PyTorch2.2.1.nn.BatchNorm1d()2.2.2.nn.BatchNorm2d()2.2.3.nn.BatchNorm3d() 2.3.其他常见的Normalization…

java servlet jsp 农产品价格信息搜集系统 python开发mysql数据库web结构jsp编程计算机网页项目

一、源码特点 jsp 农产品价格信息搜集系统 python是一套完善的java web信息管理系统&#xff0c;对理解JSP java编程开发语言有帮助 系统采用 serlvetdaobean 模式开发 利用python 进行网站爬取 &#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开…

Redis常见命令有哪些?怎么使用?

一、概述&#xff1a; 在了解Redis命令之前&#xff0c;我们要先了解Redis的数据结构&#xff0c;Redis是NoSQL数据库&#xff0c;采用了json存储模式&#xff0c;比MySQL等关系数据库更易于扩展&#xff0c;拥有丰富的数据类型&#xff0c;分基本类型与特殊类型。基本类型包括…

【Linux】网络---->套接字编程(TCP)

套接字编程TCP TCP的编程流程TCP的接口TCP的代码&#xff08;单线程、多进程、多线程代码&#xff09;单线程多进程多线程 TCP的编程流程 TCP的编程流程&#xff1a;大致可以分为五个过程&#xff0c;分别是准备过程、连接建立过程、获取新连接过程、消息收发过程和断开过程。 …

【youcans的深度学习 07】PyTorch入门教程:张量的基本操作 2

欢迎关注『youcans的深度学习』系列&#xff0c;持续更新中… 【youcans的深度学习 01】安装环境之 miniconda 【youcans的深度学习 02】PyTorch CPU版本安装与环境配置 【youcans的深度学习 03】PyTorch CPU版本安装与环境配置 【youcans的深度学习 04】PyTorch入门教程&#…

面向对象程序设计概述

&#x1f9d1;‍&#x1f4bb;CSDN主页&#xff1a;夏志121的主页 &#x1f4cb;专栏地址&#xff1a;Java核心技术专栏 目录 一、类 二、对象 三、识别类 四、类之间的关系 面向对象程序设计&#xff08;Object-Oriented Programming,OOP)是当今的主流程序设计范型&#x…

线段树详解

目录 线段树的概念 线段树的实现 线段树的存储 需要4n大小的数组 线段树的区间是确定的 线段树的难点在于lazy操作 代码样例 线段树的概念 线段树&#xff08;Segment Tree&#xff09;是一种平衡二叉树&#xff0c;用于解决区间查询问题。它将一个区间划分成若干个子区…

Android 车载值不值得入手学?

前言 随着智能车的不断普及和智能化程度的提高&#xff0c;车载系统也在逐步升级和演进&#xff0c;越来越多的汽车厂商开始推出采用Android系统的车载设备&#xff0c;这为Android车载开发提供了广泛的市场需求。 其次&#xff0c;随着人工智能技术的发展和应用&#xff0c;…

Linux : 安装源码包

安装源码包之前我们要准备好yum环境&#xff0c;或者使用默认上网下载的yum仓库或者查看&#xff1a;Linux&#xff1a;rpm查询安装 && yum安装_鲍海超的博客-CSDN博客 准备离线yum仓库 &#xff0c;默认的需要在有网环境下才能去网上下载 其次就是安装 gcc make 准…

UDP协议 sendto 和 recvfrom 浅析与示例

UDP&#xff08;user datagram protocol&#xff09;用户数据报协议&#xff0c;属于传输层。 UDP是面向非连接的协议&#xff0c;它不与对方建立连接&#xff0c;而是直接把数据报发给对方。UDP无需建立类如三次握手的连接&#xff0c;使得通信效率很高。因此UDP适用于一次传…

Kyligence Zen 一站式指标平台体验——“绝对实力”的指标分析和管理工具——入门体验评测

&#x1f996;欢迎观阅本本篇文章&#xff0c;我是Sam9029 文章目录 前言Kyligence Zen 是什么Kyligence Zen 能做什么Kyligence Zen 优势在何处 正文注册账号平台功能模块介绍指标图表新建指标指标模板 目标仪表盘数据设置 实际业务体验---使用官网数据范例使用流程归因分析指…

MySQL --- 多表设计

关于单表的操作(包括单表的设计、单表的增删改查操作)我们就已经学习完了。接下来我们就要来学习多表的操作&#xff0c;首先来学习多表的设计。 项目开发中&#xff0c;在进行数据库表结构设计时&#xff0c;会根据业务需求及业务模块之间的关系&#xff0c;分析并设计表结构…

ChatGPT-4怎么对接-ChatGPT-4强化升级了哪些功能

ChatGPT-4怎么使用 使用ChatGPT-4&#xff0c;需要通过OpenAI的API接口来对接ChatGPT-4。OpenAI是一个人工智能公司&#xff0c;为开发者提供多个API接口&#xff0c;包括自然语言处理&#xff0c;图像处理等。ChatGPT-4是OpenAI开发的最新版本的聊天式对话模型&#xff0c;可…

React antd Form item「受控组件与非受控组件」子组件 defaultValue 不生效等问题总结

一、为什么 Form.Item 下的子组件 defaultValue 不生效&#xff1f; 当你为 Form.Item 设置 name 属性后&#xff0c;子组件会转为受控模式。因而 defaultValue 不会生效。你需要在 Form 上通过 initialValues 设置默认值。name 字段名&#xff0c;支持数组 类型&#xff1a;N…