【Lintcode】1057. Network Delay Time

题目地址:

https://www.lintcode.com/problem/network-delay-time/description

给定一个非负权边的简单有向图,图顶点编号是 1 ∼ N 1\sim N 1N,再给定一个顶点编号 K K K,问从 K K K到达别的所有顶点的最远距离是多少。如果有顶点到不了则返回 − 1 -1 1

可以用Dijkstra算法。思路是,先初始化一个数组dis,记录每个顶点与 K K K的距离,每个位置都初始化为正无穷,只有第 K K K个初始化为 0 0 0,表示自己到自己的距离是 0 0 0。再初始化一个数组visited,表示最短路径长度是否已经求出,初始化为false(注意这里不把顶点 K K K初始化为true,主要是具体的代码实现的需要)。接下来重复下面的循环操作:
寻找visitedfalse的所有顶点中dis最小的那个顶点,比如说是 V V V,那么 V V V的最短路径长度已经求出来了,标记其visitedtrue,并用其更新其邻接点的最短路径长度。“用其更新“的意思是,如果 V V V有个邻接点 W W W,那么源点 K K K V V V的最短路长度加上 ( V , W ) (V,W) (V,W)的长度即成为一个 K K K W W W的路径长度。这个长度有可能是比dis中记录的长度要更小的,如果更小,就对dis中记录的长度进行更新。

当循环跳出后,dis里可能存在正无穷,表示的意思是从 K K K不存在路径到达这个点。

关于如何寻找visitedfalse的所有顶点中dis最小的那个顶点,Dijkstra算法可以分为朴素Dijkstra算法和堆优化版Dijkstra算法。

法1:朴素Dijkstra。这里的寻找方式是暴力搜索dis数组。代码如下:

import java.util.*;

public class Solution {
    /**
     * @param times: a 2D array
     * @param N:     an integer
     * @param K:     an integer
     * @return: how long will it take for all nodes to receive the signal
     */
    public int networkDelayTime(int[][] times, int N, int K) {
        // Write your code here
        // 为了方便,这里把顶点编号定义为是从0到N - 1的,也就是将题目所给编号向负方向平移一位
        // dis记录编号对应的顶点与顶点K的距离,先初始化为正无穷
        int[] dis = new int[N];
        Arrays.fill(dis, Integer.MAX_VALUE);
        // 顶点K对应的距离初始化为0
        dis[K - 1] = 0;
        
        // 用邻接表建图。key代表边的出发点,value的list的每一项代表边的到达点以及边的长度
        Map<Integer, List<int[]>> graph = buildGraph(times);
        
        // visited代表编号对应的顶点是否已经算出了最短路长度。一开始都是false
        boolean[] visited = new boolean[N];
        
        while (true) {
        	// 先寻找visited是false的dis最小的点
            int curdis = Integer.MAX_VALUE, cur = -1;
            for (int i = 0; i < N; i++) {
                if (!visited[i] && dis[i] < curdis) {
                    curdis = dis[i];
                    cur = i;
                }
            }
            
            // 如果cur没有被更新,要么所有顶点的最短路都被算出来了,
            // 要么没算出来的顶点到不了
            if (cur == -1) {
                break;
            }
            // cur的最短路算出来了,将其标记为true,并用它来更新其邻接点的最短路长度
            visited[cur] = true;
            // 判断一下cur确实有能走一步到达的点(不判断就NPE了)
            if (graph.containsKey(cur)) {
                for (int[] next : graph.get(cur)) {
                	// 得到邻接点编号,以及从cur到这个点的边的长度
                    int nextPoint = next[0], costFromCur = next[1];
                    // 如果这个点没被算出最短路,则将其更新
                    // 这步判断即使不做程序也是对的,只不过如果一个点已经被算出最短路,
                    // 再去更新相当于什么也没做。判断一下可以节省这个没必要的计算
                    if (!visited[nextPoint]) {
                        dis[nextPoint] = Math.min(dis[nextPoint], dis[cur] + costFromCur);
                    }
                }
            }
        }
        
        // 求最远的顶点的最短路长度
        int res = 0;
        for (int d : dis) {
            res = Math.max(res, d);
        }
        
        // 含正无穷说明有的顶点到不了,返回-1,否则返回res
        return res == Integer.MAX_VALUE ? -1 : res;
    }
    
    private Map<Integer, List<int[]>> buildGraph(int[][] times) {
        Map<Integer, List<int[]>> graph = new HashMap<>();
        for (int[] time : times) {
            int s = time[0] - 1, t = time[1] - 1, dis = time[2];
            graph.putIfAbsent(s, new ArrayList<>());
            graph.get(s).add(new int[]{t, dis});
        }
        
        return graph;
    }
}

时间复杂度 O ( V 2 + E ) O(V^2+E) O(V2+E),建图花了 O ( V + E ) O(V+E) O(V+E)的时间,至于Dijkstra算法本身的时间复杂度事实上是 O ( V 2 ) O(V^2) O(V2),空间 O ( V + E ) O(V+E) O(V+E)

法2:堆优化版Dijkstra算法。这里的寻找方式是,开一个最小堆,按照顶点与源点的已知最短路的距离排序,当更新完某个点的dis时,将其与其最短路距离做成一个pair,一起加入一个最小堆中(这会造成)。这样每次从堆中取出pair的时候,就已经找到了最近的点。但这里要注意,堆顶的visited未必是false(原因是同一个顶点可能会被更新多次,从而加入堆多次。而要更新一个已经在堆里的元素是很难做到的),所以遇到true(也就是算过最短路)的时候需要跳过该次循环。代码如下:

import java.util.*;

public class Solution {
    /**
     * @param times: a 2D array
     * @param N:     an integer
     * @param K:     an integer
     * @return: how long will it take for all nodes to receive the signal
     */
    public int networkDelayTime(int[][] times, int N, int K) {
        // Write your code here
        int[] dis = new int[N];
        Arrays.fill(dis, Integer.MAX_VALUE);
        dis[K - 1] = 0;
        
        Map<Integer, List<int[]>> graph = buildGraph(times);
        boolean[] visited = new boolean[N];
        // 构造一个最小堆,堆里存放的是顶点编号和其当前已经算出的最短路路径长度;长度小者优先
        PriorityQueue<int[]> minHeap = new PriorityQueue<>((p1, p2) -> Integer.compare(p1[1], p2[1]));
        // 把源点自己加入堆
        minHeap.offer(new int[]{K - 1, 0});
        
        // 记录最终答案
        int res = 0;
        while (!minHeap.isEmpty()) {
        	// 取出堆顶,堆顶是未计算出最短路长度的顶点中离源点最近的那个的备选,
        	// 因为还需要判断一下其visited是否是false
            int[] curV = minHeap.poll();
            int v = curV[0];
            // 如果算出过,则略过该点
            if (visited[v]) {
                continue;
            }
            
            // 否则该点要标记为算出过,更新res,并且将未算过的点的数量减去1
            visited[v] = true;
            res = curV[1];
            N--;
            
            if (graph.containsKey(v)) {
                for (int[] next : graph.get(v)) {
                    int nextv = next[0], disFromV = next[1];
                    // 如果这个顶点未算过,并且可以更新,则对其更新并加入堆
                    if (!visited[nextv] && dis[v] + disFromV < dis[nextv]) {
                        dis[nextv] = dis[v] + disFromV;
                        minHeap.offer(new int[]{nextv, dis[nextv]});
                    }
                }
            }
        }
        
        // 如果N不等于0说明有些点到不了,返回-1;否则返回res
        return N == 0 ? res : -1;
    }
    
    private Map<Integer, List<int[]>> buildGraph(int[][] times) {
        Map<Integer, List<int[]>> graph = new HashMap<>();
        for (int[] time : times) {
            int s = time[0] - 1, t = time[1] - 1, dis = time[2];
            graph.putIfAbsent(s, new ArrayList<>());
            graph.get(s).add(new int[]{t, dis});
        }
        
        return graph;
    }
}

时间复杂度 O ( E log ⁡ E + V + E ) O(E\log E+V+E) O(ElogE+V+E),建图花了 O ( V + E ) O(V+E) O(V+E)的时间,Dijkstra算法本身花了 O ( E log ⁡ E ) O(E\log E) O(ElogE)的时间。空间 O ( V + E ) O(V+E) O(V+E)

算法正确性证明:
d d d数组即为算法算出的每个点与源点 s s s的最短路距离, δ ( u , v ) \delta(u,v) δ(u,v)表示从 u u u v v v的真正的最短路距离, w ( u , v ) w(u,v) w(u,v)表示边 u → v u\to v uv的长度。只需证明算法结束后 d [ v ] = δ ( s , v ) d[v]=\delta(s,v) d[v]=δ(s,v)对任意 v v v都成立。

首先,算法计算出的 d [ v ] d[v] d[v]是某一条从 s s s v v v的路径长度,所以 d [ v ] ≥ δ ( s , v ) d[v]\ge \delta(s,v) d[v]δ(s,v)。我们只需证明在将 v v vvisited标记为true的时候, d [ v ] = δ ( s , v ) d[v]= \delta(s,v) d[v]=δ(s,v)即可。
按照visited标记为true的先后顺序对顶点做数学归纳法。第一个被标记的点是 s s s自己,而 d [ s ] d[s] d[s]恰好是 0 0 0,结论正确。假设结论对某个顶点不成立,我们挑出第一个不成立的那个顶点 u u u,所以 d [ u ] > δ ( s , u ) d[u]>\delta(s,u) d[u]>δ(s,u)。假设从 s s s u u u的真实最短路径是 P P P,设 u u u之前被标记过的点的集合是 S S S,找到 P P P中第一次从 S S S中的点到 S S S外的点的边,记为 x → y x\to y xy。由归纳假设知 d [ x ] = δ ( s , x ) d[x]=\delta(s,x) d[x]=δ(s,x)。而 P P P是个最短路,所以 δ ( s , y ) = δ ( s , x ) + w ( x , y ) \delta(s,y)=\delta(s,x)+w(x,y) δ(s,y)=δ(s,x)+w(x,y)(这是因为最短路有最优子结构)。而算法保证了在将 x x x标记之后, d [ y ] d[y] d[y]也会得到更新成为 δ ( s , y ) \delta(s,y) δ(s,y)。显然 δ ( s , y ) ≤ δ ( s , u ) \delta(s,y)\le \delta(s,u) δ(s,y)δ(s,u)。由于 y ∉ S y\notin S y/S u ∉ S u\notin S u/S,而 u u u y y y更先被标记(即更先出优先队列),所以 d [ u ] ≤ d [ y ] d[u]\le d[y] d[u]d[y],所以 d [ u ] ≤ d [ y ] = δ ( s , y ) ≤ δ ( s , u ) d[u]\le d[y]=\delta(s,y)\le \delta(s,u) d[u]d[y]=δ(s,y)δ(s,u) d [ u ] > δ ( s , u ) d[u]>\delta(s,u) d[u]>δ(s,u)矛盾。所以算法正确。

注解:我们可以看出,当图是稀疏图的时候,堆优化版的算法效率更高;而当图很稠密的时候(这个时候 E ≈ V 2 E\approx V^2 EV2),朴素的算法效率更高。而大多数情况下我们遇到的图都是稀疏图,所以通常都用堆优化版的算法。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值