题目地址:
https://www.lintcode.com/problem/network-delay-time/description
给定一个非负权边的简单有向图,图顶点编号是 1 ∼ N 1\sim N 1∼N,再给定一个顶点编号 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
,主要是具体的代码实现的需要)。接下来重复下面的循环操作:
寻找visited
是false
的所有顶点中dis
最小的那个顶点,比如说是
V
V
V,那么
V
V
V的最短路径长度已经求出来了,标记其visited
为true
,并用其更新其邻接点的最短路径长度。“用其更新“的意思是,如果
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不存在路径到达这个点。
关于如何寻找visited
是false
的所有顶点中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
u→v的长度。只需证明算法结束后
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
v的visited
标记为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
x→y。由归纳假设知
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 E≈V2),朴素的算法效率更高。而大多数情况下我们遇到的图都是稀疏图,所以通常都用堆优化版的算法。