Network Delay Time

Medium Graphs

Problem

Given a list of travel times as directed edges times where times[i] = (u, v, w) represents the time w to travel from node u to node v, and given the number of nodes n and a starting node k, return the time it takes for all nodes to receive the signal sent from node k. If it is impossible for all nodes to receive the signal, return -1.

  • 1 ≤ k ≤ n ≤ 100
  • 1 ≤ times.length ≤ 6000
  • times[i].length == 3
  • 1 ≤ u, v ≤ n
  • u != v
  • 0 ≤ w ≤ 100
  • All pairs (u, v) are unique.

Example

Input: times = [[2,1,1],[2,3,1],[3,4,1]], n = 4, k = 2
Output: 2

Starting from node 2, the signal reaches node 1 in 1 unit of time and node 3 in 1 unit of time. From node 3, the signal reaches node 4 in 1 additional unit of time. The longest time among all nodes to receive the signal is 2, which is the answer.

Approach

Straightforward Solution

A brute-force approach would attempt to explore all paths from k to every node, which is computationally expensive and inefficient, especially with cycles and multiple paths.

Core Observation

The problem reduces to finding the shortest path from a single source node k to all other nodes in a weighted directed graph, where edge weights represent travel times.

Path to Optimal

Recognizing the problem as a shortest path problem on a weighted graph suggests using Dijkstra's algorithm. The key insight is to use a priority queue (min-heap) to always explore the next closest node, ensuring that once a node is visited, the shortest path to it is finalized. This avoids redundant work and guarantees optimality.

Optimal Approach

Build an adjacency list from the input edges for efficient neighbor lookup. Use a min-heap initialized with the starting node k at distance 0. Repeatedly pop the node with the smallest tentative distance, skip if already visited, and update the maximum time seen. For each neighbor, if unvisited, push the cumulative distance into the heap. After processing, if all nodes are visited, return the maximum distance; otherwise, return -1.

Time

O(E log V)

Each edge is processed at most once when its source node is popped from the heap, and each heap operation takes O(log V) time, where V is the number of nodes and E is the number of edges.

Space

O(V + E)

The adjacency list stores all edges (O(E)), and the heap and visited set store nodes (O(V)). This space is necessary to represent the graph and track visited nodes.

Pattern Spotlight

Dijkstra's Algorithm (Single-Source Shortest Path)

Always expand the closest unvisited node next using a min-heap to guarantee that once a node is visited, its shortest path is finalized, enabling efficient and correct shortest path computation.

Solution

1import collections
2import heapq
3
4class Solution:
5 def networkDelayTime(self, times: list[list[int]], n: int, k: int) -> int:
6 edges = collections.defaultdict(list)
7 for u, v, w in times:
8 edges[u].append((v, w))
9
10 min_heap = [(0, k)]
11 visit = set()
12 t = 0
13 while min_heap:
14 w1, n1 = heapq.heappop(min_heap)
15 if n1 in visit:
16 continue
17 visit.add(n1)
18 t = max(t, w1)
19
20 for n2, w2 in edges[n1]:
21 if n2 not in visit:
22 heapq.heappush(min_heap, (w1 + w2, n2))
23
24 return t if len(visit) == n else -1

Step-by-Step Solution

1

Build the Graph as an Adjacency List for Efficient Neighbor Lookup

6edges = collections.defaultdict(list)
7for u, v, w in times:
8 edges[u].append((v, w))

Objective

To transform the edge list into a structure that allows quick access to neighbors and their travel times.

Key Insight

An adjacency list maps each node to a list of its outgoing edges, enabling O(1) average-time access to neighbors during traversal. This structure is essential for efficient graph algorithms like Dijkstra's, avoiding repeated scanning of the entire edge list.

Interview Quick-Check

Core Logic

Using a defaultdict of lists to store edges allows efficient iteration over neighbors during the shortest path search.

Common Pitfalls & Bugs

Forgetting to build the adjacency list or using an inefficient data structure can lead to slower neighbor lookups and increased time complexity.

2

Initialize Min-Heap and Visited Set to Track Exploration State

10min_heap = [(0, k)]
11visit = set()
12t = 0

Objective

To prepare the data structures that manage the order of node exploration and prevent revisiting nodes.

Key Insight

The min-heap prioritizes nodes with the smallest current distance, ensuring the algorithm explores nodes in order of increasing distance from the source. The visited set prevents processing a node more than once, which is crucial because the first time a node is popped from the heap, its shortest path is finalized.

Interview Quick-Check

Core Logic

The min-heap stores tuples of (distance, node), enabling efficient retrieval of the closest unvisited node.

State & Boundaries

The visited set ensures each node is processed only once, preventing infinite loops and redundant work.

3

Process Nodes in Order of Increasing Distance and Update Maximum Delay

13while min_heap:
14 w1, n1 = heapq.heappop(min_heap)
15 if n1 in visit:
16 continue
17 visit.add(n1)
18 t = max(t, w1)

Objective

To iteratively explore the graph, finalizing shortest paths and tracking the longest time needed for the signal to reach any node.

Key Insight

By popping the node with the smallest tentative distance, the algorithm finalizes the shortest path to that node. Updating the maximum delay with the current node's distance captures the longest shortest path from the source to any reachable node. This step embodies the core of Dijkstra's algorithm.

Interview Quick-Check

Core Logic

Processing nodes in ascending order of distance guarantees that the first time a node is visited, its shortest path is known.

Common Pitfalls & Bugs

Skipping nodes already visited is essential to avoid processing stale entries in the heap that do not represent the shortest path.

State & Boundaries

Updating the maximum delay at each visited node ensures the final answer reflects the longest shortest path.

4

Push Neighboring Nodes into the Heap with Updated Distances

20for n2, w2 in edges[n1]:
21 if n2 not in visit:
22 heapq.heappush(min_heap, (w1 + w2, n2))

Objective

To explore all reachable neighbors by adding them to the heap with their cumulative distances from the source.

Key Insight

For each neighbor of the current node, if it has not been visited, the algorithm pushes the sum of the current node's distance and the edge weight into the heap. This ensures that all possible paths are considered, but only the shortest path will be finalized due to the heap's ordering and the visited set.

Interview Quick-Check

Core Logic

Adding neighbors with updated distances to the heap allows the algorithm to explore all potential shortest paths in order.

Common Pitfalls & Bugs

Failing to check if a neighbor is already visited before pushing can lead to redundant processing and incorrect results.

5

Return the Maximum Delay if All Nodes Are Visited, Otherwise Return -1

24return t if len(visit) == n else -1

Objective

To determine if the signal reached all nodes and return the appropriate result.

Key Insight

If the number of visited nodes equals n, it means all nodes are reachable from the source, and the maximum delay recorded is the answer. Otherwise, some nodes are unreachable, and the problem requires returning -1 to indicate impossibility.

Interview Quick-Check

Core Logic

Comparing the size of the visited set to n confirms whether all nodes received the signal.

State & Boundaries

Returning -1 when not all nodes are visited correctly handles disconnected graphs.

Line Analysis

This solution has 5 Critical lines interviewers watch for.

Line 14 Critical
w1, n1 = heapq.heappop(min_heap)

Pop the node with the smallest tentative distance from the heap.

Extracting the closest unvisited node is the core operation of Dijkstra's algorithm, guaranteeing that the shortest path to this node is finalized.

Line 24 Critical
return t if len(visit) == n else -1

Return the maximum delay if all nodes are visited; otherwise, return -1.

This conditional return correctly handles disconnected graphs by returning -1 when some nodes are unreachable, and otherwise returns the longest shortest path as the answer.

Line 10 Critical
min_heap = [(0, k)]

Initialize a min-heap with a tuple representing the starting node k at distance 0.

The min-heap prioritizes nodes by their current shortest known distance from the source, enabling Dijkstra's algorithm to explore nodes in order of increasing distance.

Line 16 Critical
continue

Mark the current node as visited.

Adding the node to the visited set finalizes its shortest path and prevents future reprocessing.

Line 21 Critical
if n2 not in visit:

Push the neighbor into the heap with the updated cumulative distance.

Adding neighbors with their cumulative distances ensures that all possible paths are considered, but the heap ordering guarantees shortest paths are finalized first.

Line 6 Core
edges = collections.defaultdict(list)

Initialize a default dictionary to store adjacency lists for each node.

Using a defaultdict of lists allows efficient storage and retrieval of all outgoing edges from each node, which is essential for graph traversal algorithms like Dijkstra's.

Line 8 Core
edges[u].append((v, w))

Append the destination node and edge weight to the adjacency list of the source node.

This step constructs the graph's adjacency list, enabling quick access to neighbors and their travel times during traversal.

Line 11 Core
visit = set()

Create a set to track visited nodes.

The visited set prevents revisiting nodes, ensuring each node's shortest path is finalized exactly once and avoiding infinite loops.

Line 15 Core
if n1 in visit:

Skip processing if the node has already been visited.

Skipping visited nodes prevents redundant work and ensures correctness by not revisiting nodes whose shortest paths are already known.

Line 20 Core
for n2, w2 in edges[n1]:

For each neighbor, check if it has not been visited.

Only unvisited neighbors are candidates for updated shortest paths and should be added to the heap for future exploration.

Line 13 Core
while min_heap:

Start a loop that continues until the min-heap is empty.

This loop processes nodes in order of increasing distance, ensuring that the shortest paths are found efficiently.

Line 17 Core
visit.add(n1)

Update the maximum delay with the current node's distance if it is larger.

Maintaining the maximum delay ensures the algorithm captures the longest shortest path from the source to any reachable node.

Line 18 Core
t = max(t, w1)

Iterate over all neighbors and their edge weights of the current node.

Exploring neighbors is necessary to propagate the shortest path calculations through the graph.

Line 12 Standard
t = 0

Initialize a variable to track the maximum delay time.

Tracking the maximum delay allows the algorithm to determine the longest shortest path from the source to any node, which is the problem's answer.

Line 7 Standard
for u, v, w in times:

Iterate over each edge in the input times list.

Processing each edge is necessary to build the adjacency list representation of the graph.

Test Your Understanding

Why does Dijkstra's algorithm guarantee that the first time a node is visited, the shortest path to it has been found?

Because the algorithm always selects the unvisited node with the smallest tentative distance from the source, any alternative path to that node would be longer or equal, so the first visit finalizes the shortest path.

Related Problems

Graphs pattern

Don't just read it. Drill it.

Reconstruct Network Delay Time from memory until it sticks. AlgoDrill blanks out key lines and makes you fill them back in, step by step.

Unlock the Network Delay Time drill

or drill a free problem