- To find the minimum time to reach all nodes, this is a single-source shortest path problem.
- The graph is directed and weighted (positive), so we can use Dijkstra's or Bellman-Ford algorithm to find the shortest path.
- It's best fit since weights are positive.
- Use a min heap to always process the adjacent node with the minimum time first.
- Maintain a time array to store the shortest known time from source to each node.
data class Edge(
val time: Int,
val node: Int
)
fun networkDelayTime(times: Array<IntArray>, n: Int, k: Int): Int {
val graph = buildGraph(times, n) // O(E) to build adjacency list
val time = IntArray(n + 1) { Int.MAX_VALUE } // 1-based index, O(V)
val minHeap = PriorityQueue(compareBy<Pair<Int, Int>> { it.first })
time[k] = 0
minHeap.add(0 to k) // O(log V)
while (minHeap.isNotEmpty()) { // O(V) iterations
val (t, node) = minHeap.poll() // O(log V)
if (time[node] < t) continue
graph[node].forEach { adj -> // O(E) iterations
if (time[adj.node] > time[node] + adj.time) {
time[adj.node] = time[node] + adj.time
minHeap.add(time[adj.node] to adj.node) // O(log V)
}
}
}
// O(V)
var ans = 0
for (node in 1..n) {
if (time[node] == Int.MAX_VALUE) return -1
ans = maxOf(ans, time[node])
}
return ans
}
private fun buildGraph(times: Array<IntArray>, n: Int): Array<HashSet<Edge>> {
val graph = Array<HashSet<Edge>>(n + 1) { hashSetOf<Edge>() }
for ((from, to, time) in times) {
graph[from].add(Edge(time, to))
}
return graph
}We use adjacency matrix to build the graph:
fun dijkstraAdjacencyMatrix(times: Array<IntArray>, n: Int, k: Int): Int {
// -1 means no edge, we have to check the possible range of weight and choose the proper value to indicate no edge.
val adjMatrix = Array(n + 1) { IntArray(n + 1) { -1 } }
for ((from, to, time) in times) {
adjMatrix[from][to] = time
}
val time = IntArray(n + 1) { Int.MAX_VALUE }
val minHeap = PriorityQueue(compareBy<Pair<Int, Int>> { it.first } )
time[k] = 0
minHeap.add(0 to k)
while (minHeap.isNotEmpty()) {
val (t, node) = minHeap.poll()
if (time[node] < t) continue
for (adjNode in adjMatrix[node].indices) {
val adjTime = adjMatrix[node][adjNode]
// We have to skip the edge if it doesn't exist.
if (adjTime == -1) continue
if (time[adjNode] > time[node] + adjTime) {
time[adjNode] = time[node] + adjTime
minHeap.add(time[adjNode] to adjNode)
}
}
}
var ans = 0
for (node in 1..n) {
if (time[node] == Int.MAX_VALUE) return -1
ans = maxOf(ans, time[node])
}
return ans
}- Time Complexity:
O((V + E) log V)- Build graph:
O(E). - Main loop:
O((V + E) log V).- Heap operations: Each node can be popped at most once, so
O(V log V)for allpoll(). - Edge processing: Each edge can be processed at most once per node, so
O(E log V)for alladd().
- Heap operations: Each node can be popped at most once, so
- Find the answer:
O(V).
- Build graph:
- Space Complexity:
O(V + E)orO(V^2)- Build graph:
O(V + E)for adjacency list,O(V^2)for adjacency matrix. - Time array:
O(V). - Min heap:
O(V).
- Build graph:
It's a variant of Dijkstra's algorithm without changing the core Dijkstra's invariant: the first time we visit a node pop from the heap, we have found its shortest distance.
There are some key differences from above implementation and the "textbook" Dijkstra's algorithm:
- Marking
visitedon pop instead of on push, this ensures us finalize a node exactly once, at its true shortest distance. - Not maintaining a
timearray (distance array):- We allow duplicate, it's OK to push the same node multiple times with different distance.
- We us
visitedto discard stale entries: The first time a node entry popped from min heap is the shortest (positive weight); later duplicate entries are skipped byvisitedset.
time(answer) ends up being the largest time of finalized shortest distance, all nodes are popped in non-decreasing order of time, the last pop is the largest time.
fun dijkstra(times: Array<IntArray>, n: Int, k: Int): Int {
val graph = buildGraph(times, n)
var remainingN = n
var time = 0
val visited = BooleanArray(n + 1) // O(V)
val minHeap = PriorityQueue<Pair<Int, Int>>(compareBy { it.first })
minHeap.add(0 to k)
while (minHeap.isNotEmpty()) {
val (t, node) = minHeap.poll() // O(log E)
// We must skip the visited (stale) node first
if (visited[node]) {
continue
}
visited[node] = true
time = maxOf(t, time) // Or we just update time = t directly, the first time we visit a node is the shortest path.
remainingN--
graph[node].forEach { (adjNode, adjTime) -> // O(E)
minHeap.add((adjTime + t) to adjNode) // O(log E)
}
}
return if (remainingN == 0) time else -1
}- Time Complexity:
O((V + E) log E)on average orO(E log E)in the worst case (dense graph,E = V^2)- Build graph:
O(E). - Main loop:
O((V + E) log E)- Heap can contain up to
O(E)entries since we always enqueue ALL neighbors for each node. - Heap operations: Each node can be popped at most once, so
O(V log E)for allpoll(). - Edge processing: We enqueue all edges for each node, so
O(E log E)for alladd().
- Heap can contain up to
- Find the answer:
O(V).
- Build graph:
- Space Complexity:
O(V + E),O(V^2)for adjacency matrix.- Build graph:
O(V + E)for adjacency list,O(V^2)for adjacency matrix. - Visited set:
O(V). - Min heap:
O(E).
- Build graph:
A -- (1)--> B
| /
(2) /
| /
C <-- (5)- We have two entries for
C:2and6. - We visit
Cat distance2first, which is the true shortest path. - The later entry
6is discarded because we have already visitedCat distance2. - The final answer is
2.
A -- (1)--> B
| /
(5) /
| /
C <-- (2)- We enqueued
Ctwice: once with5(direct) and later with3(viaB). - We visit
Cat distance3first, becase we pop the shorter entry (3 to C) over the longer entry (5 to C) from the min heap. - Mark
Cas visited. - The later entry
5is discarded because we have already visitedCat distance3. - The final answer is
3.
TODO: Review the notes + implementations
fun spfa(times: Array<IntArray>, n: Int, k: Int): Int {
val graph = buildGraph(times, n)
val queue = ArrayDeque<Int>() // We use queue, not min heap.
val time = IntArray(n + 1) { Int.MAX_VALUE }
time[k] = 0
queue.add(k)
while (queue.isNotEmpty()) {
val node = queue.removeFirst()
graph[node].forEach { (adjTime, adjNode) ->
// We relax the edge and enqueue the adjacent node, if the new distance is shorter.
if (time[adjNode] > time[node] + adjTime) {
time[adjNode] = time[node] + adjTime
queue.addLast(adjNode)
}
}
}
var ans = 0
for (node in 1..n) {
if (time[node] == Int.MAX_VALUE) return -1
ans = maxOf(ans, time[node])
}
return ans
}TODO: Review the notes + implementations
fun networkDelayTime(times: Array<IntArray>, n: Int, k: Int): Int {
// Here we won't use Int.MAX_VALUE because it will break the relaxation, since Int.MAX_VALUE + any number will be Int.MIN_VALUE, it makes the result wrong.
val infinite = Int.MAX_VALUE / 2
val times = IntArray(n + 1) { _ -> infinite }
times[k] = 0
for (i in 1..n - 1) {
for (edge in times) {
val u = edge[0]
val v = edge[1]
val weight = edge[2]
if (times[v] > times[u] + weight) {
times[v] = times[u] + weight
}
}
}
var max = Int.MIN_VALUE
for (vertex in 1..n) {
val d = times[vertex]
max = if (d > max) d else max
}
return if (max == infinite) -1 else max
}The following implementations are from LeetCode solutions.
Key Pattern: Uses popped heap value directly, no distance array
public int networkDelayTime(int[][] times, int N, int K) {
Map<Integer, Map<Integer,Integer>> map = new HashMap<>();
for(int[] time : times){
map.putIfAbsent(time[0], new HashMap<>());
map.get(time[0]).put(time[1], time[2]);
}
//distance, node into pq
Queue<int[]> pq = new PriorityQueue<>((a,b) -> (a[0] - b[0]));
pq.add(new int[]{0, K});
boolean[] visited = new boolean[N+1];
int res = 0;
while(!pq.isEmpty()){
int[] cur = pq.remove();
int curNode = cur[1];
int curDist = cur[0];
if(visited[curNode]) continue;
visited[curNode] = true;
res = curDist; // Uses popped value directly
N--;
if(map.containsKey(curNode)){
for(int next : map.get(curNode).keySet()){
pq.add(new int[]{curDist + map.get(curNode).get(next), next});
}
}
}
return N == 0 ? res : -1;
}Key Pattern: Classic Dijkstra with both distance array and visited set
typedef pair<int, int> pii;
class Solution {
public:
int networkDelayTime(vector<vector<int>>& times, int n, int k) {
vector<vector<pii> > g(n + 1);
for (const auto& t : times) {
g[t[0]].emplace_back(t[1], t[2]);
}
const int inf = 1e9;
vector<int> dist(n + 1, inf);
vector<bool> vis(n + 1, false);
dist[k] = 0;
priority_queue<pii, vector<pii>, greater<pii> > pq;
pq.emplace(0, k);
int u, v, w;
while (!pq.empty()) {
u = pq.top().second; pq.pop();
if (vis[u]) continue;
vis[u] = true;
for (auto& to : g[u]) {
v = to.first, w = to.second;
if (dist[v] > dist[u] + w) {
dist[v] = dist[u] + w;
pq.emplace(dist[v], v);
}
}
}
int ans = *max_element(dist.begin() + 1, dist.end());
return ans == inf ? -1 : ans;
}
};Key Pattern: Checks visited and distance before enqueuing (reduces heap size)
public int networkDelayTime_Dijkstra(int[][] times, int N, int K) {
Map<Integer, List<int[]>> graph = new HashMap<>();
for (int[] edge: times) {
graph.putIfAbsent(edge[0], new ArrayList<>());
graph.get(edge[0]).add(new int[]{edge[1], edge[2]});
}
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> (a[0] - b[0]));
boolean[] visited = new boolean[N + 1];
int[] minDis = new int[N + 1];
Arrays.fill(minDis, Integer.MAX_VALUE);
minDis[K] = 0;
pq.offer(new int[]{0, K});
int max = 0;
while (!pq.isEmpty()) {
int[] curr = pq.poll();
int currNode = curr[1];
if (visited[currNode]) continue;
visited[currNode] = true;
int currDis = curr[0];
max = currDis;
N--;
if (!graph.containsKey(currNode)) continue;
for (int[] next : graph.get(currNode)) {
if (!visited[next[0]] && currDis + next[1] < minDis[next[0]]) // Conservative check
pq.offer(new int[]{currDis + next[1], next[0]});
}
}
return N == 0 ? max : -1;
}void dijkstra() {
// 起始先将所有的点标记为「未更新」和「距离为正无穷」
Arrays.fill(vis, false);
Arrays.fill(dist, INF);
// 只有起点最短距离为 0
dist[k] = 0;
// 使用「优先队列」存储所有可用于更新的点
// 以 (点编号, 到起点的距离) 进行存储,优先弹出「最短距离」较小的点
PriorityQueue<int[]> q = new PriorityQueue<>((a,b)->a[1]-b[1]);
q.add(new int[]{k, 0});
while (!q.isEmpty()) {
// 每次从「优先队列」中弹出
int[] poll = q.poll();
int id = poll[0], step = poll[1];
// 如果弹出的点被标记「已更新」,则跳过
if (vis[id]) continue;
// 标记该点「已更新」,并使用该点更新其他点的「最短距离」
vis[id] = true;
for (int i = he[id]; i != -1; i = ne[i]) {
int j = e[i];
if (dist[j] > dist[id] + w[i]) {
dist[j] = dist[id] + w[i];
q.add(new int[]{j, dist[j]});
}
}
}
}问:为什么代码要判断 dx > dis[x]?
答:对于同一个 x,例如先入堆一个比较大的 dis[x]=10,后面又把 dis[x] 更新成 5,之后这个 5 会先出堆,然后再把 10 出堆。10 出堆时候是没有必要去更新周围邻居的最短路的,因为 5 出堆之后,就已经把邻居的最短路更新过了,用 10 是无法把邻居的最短路变得更短的,所以直接 continue。
Key Pattern: Uses distance check instead of visited set (most interview-friendly)
def networkDelayTime(self, times: List[List[int]], n: int, k: int) -> int:
g = [[] for _ in range(n)] # 邻接表
for x, y, d in times:
g[x - 1].append((y - 1, d))
dis = [inf] * n
dis[k - 1] = 0
h = [(0, k - 1)]
while h:
dx, x = heappop(h)
if dx > dis[x]: # x 之前出堆过
continue
for y, d in g[x]:
new_dis = dx + d
if new_dis < dis[y]:
dis[y] = new_dis # 更新 x 的邻居的最短路
heappush(h, (new_dis, y))
mx = max(dis)
return mx if mx < inf else -1def networkDelayTime(self, times: List[List[int]], n: int, k: int) -> int:
g = [[] for _ in range(n)]
for x, y, time in times:
g[x - 1].append((y - 1, time))
dist = [float('inf')] * n
dist[k - 1] = 0
q = [(0, k - 1)]
while q:
time, x = heapq.heappop(q)
if dist[x] < time:
continue
for y, time in g[x]:
if (d := dist[x] + time) < dist[y]:
dist[y] = d
heapq.heappush(q, (d, y))
ans = max(dist)
return ans if ans < float('inf') else -1Key Pattern: Traditional Dijkstra without heap, scans all nodes each iteration
public int networkDelayTime(int[][] times, int n, int k) {
final int INF = Integer.MAX_VALUE / 2; // 防止加法溢出
int[][] g = new int[n][n]; // 邻接矩阵
for (int[] row : g) {
Arrays.fill(row, INF);
}
for (int[] t : times) {
g[t[0] - 1][t[1] - 1] = t[2];
}
int maxDis = 0;
int[] dis = new int[n];
Arrays.fill(dis, INF);
dis[k - 1] = 0;
boolean[] done = new boolean[n];
while (true) {
int x = -1;
for (int i = 0; i < n; i++) {
if (!done[i] && (x < 0 || dis[i] < dis[x])) {
x = i;
}
}
if (x < 0) {
return maxDis; // 最后一次算出的最短路就是最大的
}
if (dis[x] == INF) { // 有节点无法到达
return -1;
}
maxDis = dis[x]; // 求出的最短路会越来越大
done[x] = true; // 最短路长度已确定(无法变得更小)
for (int y = 0; y < n; y++) {
// 更新 x 的邻居的最短路
dis[y] = Math.min(dis[y], dis[x] + g[x][y]);
}
}
}Distance Array Approach:
// Your current implementation
val time = IntArray(n + 1) { Int.MAX_VALUE }
// ... process and update time array
var ans = 0
for (node in 1..n) {
if (time[node] == Int.MAX_VALUE) return -1
ans = maxOf(ans, time[node])
}Heap Value Only Approach:
// Alternative approach
var time = 0
while (minHeap.isNotEmpty()) {
val (t, node) = minHeap.poll()
if (visited[node]) continue
visited[node] = true
time = t // Use popped value directly
// ... process neighbors
}-
Key Insight: Both work because Dijkstra processes nodes in non-decreasing order of distance. The last processed node will have the maximum distance.
-
Recommendation: Use Distance Array for interviews - it's more explicit and what interviewers expect.
Visited Approach:
val visited = BooleanArray(n + 1)
// ... in loop
if (visited[node]) continue
visited[node] = trueDistance Check Approach:
// Alternative approach
if (time[node] < t) continue-
Key Insight: Both achieve the same goal - ensuring each node is processed exactly once at its shortest distance. The distance check is more efficient as it doesn't require an extra array.
-
Recommendation: Use Distance Check (
if (time[node] < t) continue) - it's the most common pattern in LeetCode solutions.
Conservative (Before Enqueuing):
graph[node].forEach { adj ->
if (!visited[adj.node] && time[node] + adj.time < time[adj.node]) {
time[adj.node] = time[node] + adj.time
minHeap.add(adj.node to time[adj.node])
}
}Lazy (Always Enqueue):
graph[node].forEach { adj ->
minHeap.add(adj.node to time[adj.node])
}-
Key Insight: The lazy approach is simpler and still correct because the distance check on pop (
if (time[node] < t) continue) handles stale entries. -
Recommendation: Use Lazy Enqueuing - always enqueue and let the distance check handle stale entries.
All variants are correct because they respect Dijkstra's fundamental property: the first time a node is popped from the min-heap, we have found its shortest distance.