[模板总结] - Dijkstra 最短路径求解

模板题链接

Leetcode 743. 网络延迟时间

经典练习题

787. Cheapest Flights Within K Stops

1928. Minimum Cost to Reach Destination in Time

Dijkstras算法介绍

1. 最短路径算法概述

在图论问题中最为经典问题之一就是求最短路径问题,今天我们主要来讨论单源点最短路径问题,求解最短路径问题首先我们需要理解问题中图的类型,通常来说构建图会有下面几种情况

  • 无边权DAG: 也就是拓扑图,这种图的最短路径通常可以直接拓扑排序并在图中不断更新

  • 一般无边权图(可能有环):这类图的最短路径可以直接利用BFS(或双向BFS)直接暴力求解最短路径

  • 有非负权图(可能有环):这类图就是本文的主题,通常可以利用Dijkstra算法求解

    • 注:这类题目也可以使用BFS/DFS求解,需要将所有路径遍历求解最后最短路径

  • 有负边权图(可能有环):这类图就可以用Bellman-Ford算法求解。

2. Dijkstra算法实现

该算法的基本核心思想是通过贪心算法来每一次找到当前距离起始点距离最近的点,并且更新该点相邻点的距离,以此循环知道所有的点都访问过,即可结束。一下两种实现方式唯一不同的只是选取当前最短距离的点方式不同,朴素法直接暴力枚举,而堆优化是利用最小堆进行选取

  1. 初始化每一个节点起始距离信息dist[i] (每一个点距离起始点的距离),除了起始点距离为0以外,其余点都为INF
  2. 找出当前距离值最近dist的点,将该点标记到已访问点中,更新该点相邻点的距离
  3. 循环Step 2直到所有点都已访问
最短路径图例
最短路径图例:来源 AlgorithmHelper by rpandey1234

2.1 朴素型算法

朴素法直接暴力枚举当前dist距离数组,找到最小值

class Solution {
    public int networkDelayTime(int[][] times, int n, int k) {

        // 建图 - 由于稀疏图,使用稀疏表进行保存
        HashMap<Integer, List<int[]>> map = new HashMap<>();
        
        for(int[] time: times) {
            int st = time[0];
            int ed = time[1];
            int cost = time[2];
            
            if(!map.containsKey(st)) map.put(st, new ArrayList<>());
            
            map.get(st).add(new int[]{ed, cost});
        }
        
        // 保存距离信息
        int[] dist = new int[n+1];
        Arrays.fill(dist, Integer.MAX_VALUE);
        // 初始化起始点k距离信息为0
        dist[k] = 0;
        int connected = 0;
        // 标记已经访问过的点,避免重复计算
        boolean[] visited = new boolean[n+1];
        while(connected>n) {
            // 找到当前最小值  
            int city = -1;
            int min = Integer.MAX_VALUE;
            // 暴力枚举
            for(int i=1; i<=n; i++) {
                if(i==k) continue;
                if(min>dist[i]) {
                    city = i;
                    min = dist[i];
                }
            }

            if(city==-1) break;
            if(visited[city]) continue;
            
            visited[city] = true;
            connected++;
            
            // update neighbors dist
            if(map.containsKey(city)) {
                List<int[]> neighbors = map.get(city);
                for(int[] neigh: neighbors) {
                    dist[neigh[0]] = Math.min(dist[neigh[0]], dist[city]+neigh[1]);
               
                }
            }            
        }
        
        return dist[n-1]==Integer.MAX_VALUE? -1: dist[n-1];
    }
}

时间复杂度:T = N * (T_{extractMin} + T_{updateNeighbors}) = N*(N + k*1) = O(N^{2}), 一共需要循环N个节点,每次循环内部N次枚举找到最小值,k次枚举更新相邻节点距离;空间复杂度:O(E + V),E是指边的个数,也就是初始建图需要的空间,V是节点数量也就是后续计算距离数组需要的空间。 

2.2 堆优化算法

另外建立一个小顶堆来保存每个点的距离信息,注意这里小顶堆中会可能保存同一个点旧的信息和新的信息,但是新的距离信息会因为更小而放置在堆顶位置,旧的距离信息并不会影响计算结果。

class Solution {
    public int networkDelayTime(int[][] times, int n, int k) {

        // 建图 - 由于稀疏图,使用稀疏表进行保存
        HashMap<Integer, List<int[]>> map = new HashMap<>();
        
        for(int[] time: times) {
            int st = time[0];
            int ed = time[1];
            int cost = time[2];
            
            if(!map.containsKey(st)) map.put(st, new ArrayList<>());
            
            map.get(st).add(new int[]{ed, cost});
        }
        
        // 保存距离信息
        int[] dist = new int[n+1];
        Arrays.fill(dist, Integer.MAX_VALUE);
        // 初始化起始点k距离信息为0
        dist[k] = 0;
        // 小顶堆用于保存和提取当前最短距离信息
        PriorityQueue<int[]> heap = new PriorityQueue<>((int[] x, int[] y) -> (x[1] - y[1]));
        heap.offer(new int[] {k, dist[k]});
        
        int max = -1;
        int connected = 0;
        // 标记已经访问过的点,避免重复计算
        boolean[] visited = new boolean[n+1];
        while(!heap.isEmpty()) {
            // 去除当前最小距离点
            int[] curr = heap.poll();
            
            if(visited[curr[0]]) continue;
            
            int city = curr[0];
            visited[city] = true;
            connected++;
            
            // update neighbors dist
            if(map.containsKey(city)) {
                List<int[]> neighbors = map.get(city);
                for(int[] neigh: neighbors) {
                    dist[neigh[0]] = Math.min(dist[neigh[0]], dist[city]+neigh[1]);
                    heap.offer(new int[]{neigh[0], dist[neigh[0]]});
                }
            }
            
        }
        
        return dist[n-1];
    }
}

时间复杂度:T = V*(T_{extractMin} + T_{deleteNodeFromHeap} + T_{addNodeInHeap}) 结果为V*(1 + logV + k*logV) = V*(1+k)*logV = (V+E)*logV,这里V是所有节点数,E是所有边数;空间复杂度:O(E+V)。 

  • 注:比较朴素法和堆优化法,可以看出如果是稠密图E \approx V^{^{2}} 时,其实堆优化反而效率比朴素法慢,但是在面试中常见问题通常是稀疏图,所以堆优化效果更好。

2.3 Dijkstra算法正确性

笔者在一开始学习Dijkstra时会困惑这个问题,为什么每一次提取当前最短距离的未收录点就一定能够保证最后结果一定是最短路径值,难道就没有可能后面取的距离稍大的点构成的路径反而更小?也就是D(S, x) > d(S, x): D(S, x)指当前计算最短路径,d(S, x)指真实最短路径。

相关证明可以参考这个资源Prove Dijkstra,我在这里概括一下,我们可以利用反证法来证明:

图片来源: Prove Dijkstra

 

  • 证明D(S, x) > d(S, x),即存在一个目前没收录的点 y, 使得 d(S, x) = dist[y] + D(y, x) 
    • D(S, x)指当前计算的从起始点 S 到 x 的最短路径
    • d(S, x)指真实从起始点 S 到 x 的最短路径
  • 已知D(S, x) = dist[x], 因为dist[x]是当前最小距离的点
  • 根据Dijkstra算法逻辑,每一次选取最小距离点,那么如果y是未收录点,那么说明dist[x]<=dist[y]
  • 也就说明d(S, x) = dist[y] + D(y,x) > dist[x] = D(S, x),d(S, x)> D(S, x)
  • 与前面假设相矛盾,不成立,所以利用该反证法可以证明Dijkstra每一次选取最小距离未收录点最后一定能得到全局最短路径。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值