复旦大学961-数据结构-第五章-图(四)最短路径问题,Dijkstra算法,Floyd算法

961全部内容链接

最短路径的概念

带权路径长度:带权图中,一个节点u到另一个节点v所经过的边的权值之和称为带权路径长度。一个带权图中,节点u到节点v有许多路径,其中权值之和最短的那一条称为最短路径。当然最短路径也可以针对无向图,比如利用BSF算法中求出节点的单源最短路径。

带权图的单源最短路径:就是求一个节点到所有其他节点的最短路径。Dijkstra算法就是干这个的。
带权图的每对顶点间的最短路径:使用Floyd算法解决。

Dijkstra算法

Dijkstra(迪杰斯特拉)可以解决带权图单源最短路径问题。该算法可以同时解决两个问题:初始节点v到其他所有节点的最短路径是多少,并且还能解决到其他节点的最短路径是什么。
该算法既可以针对无向图也可以针对有向图。无向图就可以理解成两个方向都可以走。

具体思想为:
在这里插入图片描述
如图,假设问题是求v1到其他各个点的最短路径。 那么v1的邻接节点有v2和v4。其中 <v1,v4>=1,<v1,v2>=2。这两个中<v1,v4>最小,所以v1到v4的最短路径一定是v1直接到v4,不管怎么绕道都不如直接过来快。之后再找v4的邻接节点 v3,v5,v6,v7。此时可以确定v2的最短路径了。这里可能会很奇怪,为什么找了v4的邻接节点就可以确定v2的最短路径。因为目前已知的最短路径为 <v1,v2>=2,<v1,v3>=3,<v1,v5>=3,<v1,v6>=9,<v1,v7>=5。这里面<v1,v2>最小,意味着不管怎么绕道,都不如v1直接到v2快。依照这个理论,就可以得到v1到所有其他节点的最短路径。

该算法需要初始三个集合:

v1v2v3v4v5v6v7
known××××××
distance0
pathnullnullnullnullnullnullnull

其中known存储的是“节点的最短路径是否已经确定下来”。distance存储的是“当前已知的v1到该节点的最短路径是多少”。path存储的是“最短路径的上一个环节是哪个节点”。比如其中v1到v6的最短路径为 v1,v4,v3,v6,那么path[v6]=v3,path[v3]=v4,path[v4]=v1,path[v1]=null。

整个算法语言描述如下(这里使用v1代指起始节点):

  1. 初始化known的Set集合,用于存储已经确定了最短路径的节点
  2. 初始化distance的Map<节点, 距离>,保存当前已知的v1到该节点的最短距离。初始条件下,只有v1为0,其他都为无穷。
  3. 初始化path的Map<节点,节点>,保存当前路径的上一个环节的节点。
  4. 进行while循环,若known集合的Size小于图中节点数的话,说明还有节点没有找到最短路径,那么继续循环,否则就跳出循环。
    4.1 从distanceMap中找出当前路径最短且不在known中的节点u。
    4.2 将这个节点u加入known,即这个节点的当前最短路径就是它最终的最短路径
    4.3 找出u的所有邻接节点
    4.4 遍历u所有的邻接节点Ni
    4.4.1 计算tempDistance = distance[u] + <u,Ni>,即计算从u节点绕路到Ni节点的距离
    4.4.2 若tempDistance比distance[Ni]的距离短,说明从v1->u->Ni更快,即从u节点绕路到Ni节点更快,那么就更新distance[Ni]=tempDistance,paht[Ni]=u。
  5. 循环结束,返回distance和path。

Java代码如下:

    public static Map dijkstra(WeightedGraph graph, Object vertex) {
        Set known = new HashSet();  // 存储都有哪些节点的最短路径已经确定下来了
        Map<Object, Integer> distance = new HashMap<>(); // 存储vertex节点到其他节点的最短路径的路径长度
        Map<Object, Object> path = new HashMap<>(); // 存储vertex到其他节点的路径。Key为节点,Value为该节点最短路径的上一个环节是哪一个节点

        for (Object itemVertex : graph.getVertexes()) {
            distance.put(itemVertex, Integer.MAX_VALUE);  // 初始化vertex节点到其他的节点的距离,初始都为无穷
            path.put(itemVertex, null);  // 初始化vertex节点的路径,最开始都为null
        }
        distance.put(vertex, 0); // 初始化vertex节点到自身的距离为0
        // 初始化到此整个结束

        while (known.size() < graph.getVertexNumber()) { // 如果节点没有确定下来最短路径,则继续循环
            Integer minDistance = Integer.MAX_VALUE;
            Object minVertex = null;
            // 找出distance中距离最小且还没有确定最短路径的节点
            for (Object key : distance.keySet()) {
                if (!known.contains(key) // 如果该节点还没有确定路径
                        && distance.get(key) <= minDistance) // 并且比当前的最短路径要短
                {
                    minDistance = distance.get(key); // 则更新当前最短路径和当前距离最小节点
                    minVertex = key;
                }
            }

            known.add(minVertex); // 上面找出的距离最小的节点且

            Object[] neighbors = graph.neighbors(minVertex); // 找出该节点的所有邻接节点
            for (Object neighbor : neighbors) {
                // 计算初始节点到该minVertex的最小路径+它邻居的路径。即求它的邻接节点从该节点绕过来所需用的最短路径
                int tempDistance = graph.getEdgeWeight(minVertex, neighbor) + distance.get(minVertex);
                if (tempDistance < distance.get(neighbor)) {
                    // 如果之前所求的初始节点到neighbor节点的距离比从当前节点绕道所走的路远,
                    // 则将neighbor的最短路径更新成从当前节点绕道走
                    distance.put(neighbor, tempDistance);
                    path.put(neighbor, minVertex);
                }
            }
        }
        return distance; // 如果返回path,则求得是最短路径是什么,返回distance求得是最短路径的长度
//        return path;
    }

复杂度分析:

  • 空间复杂度:申请了三个集合,每个集合存储的都是节点,所以空间复杂度为O(|V|)
  • 时间复杂度:根据上面的算法,有两处循环,一个是while循环所有节点,while里面是查找邻接节点。所以不同的存储方式的差异主要在查找邻接节点上,如果是使用邻接矩阵存储,那时间复杂度为O(|V|^2)

适用性:dijkstra算法不适用于带负权值的图。比如:
在这里插入图片描述
对于该图,如果用dijkstra算法求得话,v0到v2的最短路径就是7,但实际上从v1绕道去v2可以更短,只需要10-5=5。

Floyd算法

Floyd算法适用于求各个节点直接的最短路径长度和各个节点之间的最短路径。它的基本想法为:

  1. 先计算所有节点都不能绕路的情况下,各个节点之间的距离
  2. 然后计算允许将v0作为中转节点,即允许往v0绕路,各个节点之间的最短距离。
  3. 然后计算允许将v0,v1作为中转节点,各个节点之间的最短距离
  4. 最后计算允许将v0,v1…vn作为中转节点,各个节点之间的最短距离。

算法实现需要借助一个二维数组。

在这里插入图片描述
比如该图,需要申请一个二维数组,初始状态如下:

数组arrv1v2v3v4v5v6v7
v1021
v20310
v3405
v420284
v506
v60
v710

该数组arr[i][j]表示<i,j> ,即节点i到节点j的当前最短距离。初始情况下,不允许绕路,所以初始数组如上表所示。

第一轮循环可以允许v1节点作为中间节点,即允许往v1节点绕路,所以即当
arr[i][j] > arr[i][v1] + arr[v1][j] 时,更新 arr[i][j] = arr[i][v1] + arr[v1][j]
这个上面提到过 arr[i][j] 就是当前已知最短路径,arr[i][v1]就是i节点到v1节点已知的最短路径,arr[v1][j]是v1节点到j节点的最短路径。如果从v1绕路更快,那么就把它换成从k走。

第二轮循环原理类似,直至所有的节点循环完毕。

同时,如果想要求出最短路径是什么,还可以再申请一个数组存储两个节点直接的中转节点。

Java代码如下:

    public static long[][] floyd(WeightedGraph graph) {
        Object[] vertexes = graph.getVertexes();  // 获取图中的所有节点

        long[][] distances = new long[vertexes.length][vertexes.length];  // 申请数组保存各节点之间的距离
        Object[][] path = new Object[vertexes.length][vertexes.length];     // 申请数组保存各个节点最短距离之间的中转节点
        for (int i = 0; i < distances.length; i++) {        // 初始化数组
            for (int j = 0; j < distances[i].length; j++) {
                distances[i][j] = graph.getEdgeWeight(vertexes[i], vertexes[j]);
                if (distances[i][j] == 0 && i!=j) {
                    distances[i][j] = Integer.MAX_VALUE; 
                }
            }
        }

        // Floyd核心代码开始
        for (int k = 0; k < vertexes.length; k++) { // k代表中转节点,从v0开始一直到vn
            for (int i = 0; i < vertexes.length; i++) {  // i代表起始节点,即路径<i,j>中的i
                for (int j = 0; j < vertexes.length; j++) { // j代表目标节点,即路径<i,j>中的j
                    if (distances[i][j] > distances[i][k] + distances[k][j]) { // 如果通过k绕路比原先的快,则更新最短路径和中转节点
                        distances[i][j] = distances[i][k] + distances[k][j];
                        path[i][j] = vertexes[k];
                    }
                }
            }
        }

        return distances; // 返回各个最短路径长度
//        return path; // 返回各个最短路径
    }

Floyd算法稍微有些难理解,理解不了就背下来,反正没几行。时间复杂度容易看出是O(|V|^3)

适用性:
Floyd算法可以使用与带负权值的图。比如上述例子:
在这里插入图片描述
若采用Dijkstra算法肯定不行,这个上面说过了。但是如果用Floyd算法就可以。第一轮初始化arr[v0][v2] = 7。然后当允许通过v1中转时,arr[v0][v2]就会变成5。

但是Floyd不适用于带负权值且有回路的图。
在这里插入图片描述
比如这个图,这个图讲道理就没有最短路径,比如v0到v1的最短路径是-9,但是你要是再绕一圈,就变成了-11,再绕一圈就变成了-13。所以根本就不存在最短路径

最短路径算法总结

BFS算法Dijkstra算法Floyd算法
无权图
带权图×
带负权值的图××
带负权值有回路的图×××
时间复杂度O(|V|^2) 或 O(|V|+|E|)O(|V|^2)O(|V|^3)
通常用于求无权图的单元最短路径求带权图的单源最短路径求带权图中各个顶点间的最短路径
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

iioSnail

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

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

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

打赏作者

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

抵扣说明:

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

余额充值