Files
codetutor/backend/data/questions/network-delay-time.yaml

256 lines
12 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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:
- slug: heap
is_optimal: true
- slug: bfs
is_optimal: false
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
&nbsp;
**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
&nbsp;
**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
&nbsp;
**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`
&nbsp;
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))."
pattern_comparison: |
**Dijkstra (Heap) vs BFS: When Weights Matter**
This problem illustrates a critical distinction in graph traversal:
| Algorithm | Time | Works With | Use When |
|-----------|------|------------|----------|
| **Dijkstra (Heap)** | O((V+E) log V) | Non-negative weighted edges | Edge weights vary |
| **BFS** | O(V + E) | Unweighted or unit-weight edges | All edges have same cost |
| **Bellman-Ford** | O(V × E) | Any weights (including negative) | Negative edges possible |
**Why BFS fails here:**
BFS explores nodes in order of *hop count* (number of edges), not *total distance*. Consider:
```
A --1--> B --1--> C
A --------10------> C
```
BFS would reach C via the direct edge first (1 hop) with cost 10, missing the shorter 2-hop path with cost 2.
**Why Dijkstra works:**
The min-heap ensures we process nodes in order of *actual distance* from the source. When we first reach a node, we've found the shortest path to it (because all unvisited nodes are at least as far away).
**The key insight:** Dijkstra is essentially "weighted BFS" — the heap replaces the queue to account for edge weights.
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.