230 lines
11 KiB
YAML
230 lines
11 KiB
YAML
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
|
|
|
|
function_signature: "def network_delay_time(times: list[list[int]], n: int, k: int) -> int:"
|
|
|
|
test_cases:
|
|
visible:
|
|
- input: { times: [[2, 1, 1], [2, 3, 1], [3, 4, 1]], n: 4, k: 2 }
|
|
expected: 2
|
|
- input: { times: [[1, 2, 1]], n: 2, k: 1 }
|
|
expected: 1
|
|
- input: { times: [[1, 2, 1]], n: 2, k: 2 }
|
|
expected: -1
|
|
hidden:
|
|
- input: { times: [[1, 2, 1], [2, 3, 2], [1, 3, 4]], n: 3, k: 1 }
|
|
expected: 3
|
|
- input: { times: [[1, 2, 1], [2, 1, 3]], n: 2, k: 2 }
|
|
expected: 3
|
|
- input: { times: [[1, 2, 1], [2, 3, 7], [1, 3, 4], [2, 1, 2]], n: 3, k: 1 }
|
|
expected: 4
|
|
- input: { times: [[3, 5, 78], [2, 1, 1], [1, 3, 0], [4, 3, 59], [5, 3, 85], [5, 2, 22], [2, 4, 23], [1, 4, 43], [4, 5, 75], [5, 1, 15], [1, 5, 91], [4, 1, 16], [3, 2, 98], [3, 4, 22], [5, 4, 31], [1, 2, 0], [2, 5, 4], [4, 2, 51], [3, 1, 36], [2, 3, 59]], n: 5, k: 5 }
|
|
expected: 31
|
|
|
|
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.
|