title: Network Delay Time slug: network-delay-time difficulty: medium leetcode_id: 743 leetcode_url: https://leetcode.com/problems/network-delay-time/ categories: - graphs - heap patterns: - bfs - heap description: | You are given a network of `n` nodes, labeled from `1` to `n`. You are also given `times`, a list of travel times as directed edges `times[i] = (u_i, v_i, w_i)`, where `u_i` is the source node, `v_i` is the target node, and `w_i` is the time it takes for a signal to travel from source to target. We will send a signal from a given node `k`. Return *the **minimum** time it takes for all the* `n` *nodes to receive the signal*. If it is impossible for all the `n` nodes to receive the signal, return `-1`. constraints: | - `1 <= k <= n <= 100` - `1 <= times.length <= 6000` - `times[i].length == 3` - `1 <= u_i, v_i <= n` - `u_i != v_i` - `0 <= w_i <= 100` - All the pairs `(u_i, v_i)` are **unique** (i.e., no multiple edges) examples: - input: "times = [[2,1,1],[2,3,1],[3,4,1]], n = 4, k = 2" output: "2" explanation: "Starting from node 2, the signal reaches node 1 and node 3 at time 1. The signal then travels from node 3 to node 4, arriving at time 2. The maximum time to reach any node is 2." - input: "times = [[1,2,1]], n = 2, k = 1" output: "1" explanation: "Starting from node 1, the signal reaches node 2 at time 1." - input: "times = [[1,2,1]], n = 2, k = 2" output: "-1" explanation: "Starting from node 2, there is no edge leading to node 1, so node 1 can never receive the signal." explanation: intuition: | Imagine you're standing at a train station and want to know the fastest time to reach every other station in a railway network. Trains run at different speeds between stations — some routes are faster than others. The key insight is that **this is a single-source shortest path problem**. We need to find the shortest time from the starting node `k` to every other node in the graph. The answer is then the maximum of all these shortest times — because that's when the last node receives the signal. Think of the signal spreading like ripples in a pond, but with weighted edges. The signal doesn't spread at uniform speed; it travels faster along some paths than others. We need to track when each node *first* receives the signal, which is the shortest path from `k` to that node. **Dijkstra's algorithm** is perfect here. It greedily processes nodes in order of their distance from the source, guaranteeing that when we first visit a node, we've found the shortest path to it. Since all edge weights are non-negative (`0 <= w_i <= 100`), Dijkstra's algorithm works correctly. approach: | We solve this using **Dijkstra's Algorithm** with a min-heap: **Step 1: Build the adjacency list** - Create a graph representation where `graph[u]` contains all `(v, w)` pairs representing edges from `u` to `v` with weight `w` - This allows O(1) lookup of all neighbors for any node   **Step 2: Initialise data structures** - `dist`: A dictionary or array to store the shortest known distance to each node (initially empty or infinity) - `heap`: A min-heap (priority queue) containing `(distance, node)` pairs - Push the starting node `k` with distance `0` onto the heap   **Step 3: Process nodes in order of distance** - Pop the node with the smallest distance from the heap - If we've already visited this node (found in `dist`), skip it — we already found a shorter path - Otherwise, record this distance as the shortest path to this node - For each neighbor, calculate the new distance through this node - If we haven't visited the neighbor yet, push `(new_distance, neighbor)` onto the heap   **Step 4: Check if all nodes are reachable** - After processing, if we've visited all `n` nodes, return the maximum distance - If any node is unreachable, return `-1`   The heap ensures we always process the node with the smallest known distance first, which is the key to Dijkstra's correctness. common_pitfalls: - title: Using BFS on a Weighted Graph description: | A common mistake is treating this like a standard BFS problem. BFS finds shortest paths in **unweighted** graphs (or graphs where all edges have the same weight). With varying edge weights, BFS might visit a node through a path with fewer edges but higher total weight. For example, a direct edge with weight 10 would be processed before a two-hop path with total weight 2. Always use Dijkstra (or Bellman-Ford) for weighted shortest paths. wrong_approach: "Standard BFS without considering edge weights" correct_approach: "Dijkstra's algorithm with a min-heap priority queue" - title: Forgetting Node Labels Start at 1 description: | The nodes are labeled `1` to `n`, not `0` to `n-1`. If you use a 0-indexed array for distances, you'll have off-by-one errors. Either use a dictionary for distances, or create an array of size `n+1` and ignore index 0. wrong_approach: "Using 0-indexed array without adjustment" correct_approach: "Use a dictionary or 1-indexed array" - title: Not Handling Unreachable Nodes description: | The graph may be disconnected — not all nodes may be reachable from `k`. You must check that all `n` nodes received the signal before returning the maximum time. If `len(dist) < n` after Dijkstra completes, some nodes are unreachable, and the answer is `-1`. wrong_approach: "Returning max distance without checking reachability" correct_approach: "Verify all n nodes are in the distance dictionary" - title: Re-processing Already Visited Nodes description: | The same node can be pushed onto the heap multiple times with different distances. When you pop a node, check if it's already been finalized (exists in your distance map). Without this check, you might process the same node repeatedly, leading to incorrect results or TLE. wrong_approach: "Processing every node popped from the heap" correct_approach: "Skip nodes that have already been assigned a shortest distance" key_takeaways: - "**Dijkstra's algorithm** is the go-to for single-source shortest paths with non-negative weights" - "**Min-heap** ensures we always process the closest unvisited node, maintaining the greedy invariant" - "The answer to 'time for all nodes to receive signal' is the **maximum** of all shortest paths" - "This pattern appears in many variations: minimum cost to reach destinations, cheapest flights, etc." time_complexity: "O((V + E) log V). Each node is processed once, and each edge triggers at most one heap operation. With `n` nodes and `m` edges, this is O((n + m) log n)." space_complexity: "O(V + E). We store the adjacency list (O(E)) and the distance dictionary plus heap (O(V))." solutions: - approach_name: Dijkstra's Algorithm is_optimal: true code: | import heapq from collections import defaultdict def network_delay_time(times: list[list[int]], n: int, k: int) -> int: # Build adjacency list: graph[u] = [(v, w), ...] graph = defaultdict(list) for u, v, w in times: graph[u].append((v, w)) # Min-heap: (distance, node), starting from node k heap = [(0, k)] # Dictionary to store shortest distance to each node dist = {} while heap: # Get the node with smallest distance d, node = heapq.heappop(heap) # Skip if we've already found a shorter path to this node if node in dist: continue # Record the shortest distance to this node dist[node] = d # Explore all neighbors for neighbor, weight in graph[node]: if neighbor not in dist: # Push new distance to neighbor onto heap heapq.heappush(heap, (d + weight, neighbor)) # Check if all nodes are reachable if len(dist) == n: # Return the maximum distance (time for last node to receive signal) return max(dist.values()) else: # Some nodes are unreachable return -1 explanation: | **Time Complexity:** O((V + E) log V) — Each edge may cause a heap push (O(log V)), and we process each node once. **Space Complexity:** O(V + E) — The adjacency list stores all edges, and the heap/distance dict store at most V entries. Dijkstra's algorithm processes nodes in order of their distance from the source. The min-heap ensures we always pick the closest unvisited node. When we pop a node, we've guaranteed found the shortest path to it (since all edge weights are non-negative). - approach_name: Bellman-Ford Algorithm is_optimal: false code: | def network_delay_time(times: list[list[int]], n: int, k: int) -> int: # Initialise distances: infinity for all except source dist = [float('inf')] * (n + 1) dist[k] = 0 # Relax all edges n-1 times for _ in range(n - 1): # Flag to detect early termination updated = False for u, v, w in times: # If we can reach v faster through u if dist[u] != float('inf') and dist[u] + w < dist[v]: dist[v] = dist[u] + w updated = True # Early exit if no updates in this round if not updated: break # Find maximum distance (excluding index 0 and unreachable nodes) max_dist = max(dist[1:]) # If any node is unreachable, return -1 return max_dist if max_dist < float('inf') else -1 explanation: | **Time Complexity:** O(V * E) — We relax all edges up to V-1 times. With n=100 and edges up to 6000, this is about 600,000 operations. **Space Complexity:** O(V) — We only store the distance array. Bellman-Ford works by repeatedly relaxing all edges. After `n-1` iterations, all shortest paths are found (since the longest simple path has at most `n-1` edges). It's slower than Dijkstra but can handle negative edge weights. Here it's shown as an alternative approach.