questions M-R

This commit is contained in:
2025-05-25 12:43:25 +01:00
parent 917c371529
commit 68699f35ec
62 changed files with 12841 additions and 0 deletions

View File

@@ -0,0 +1,209 @@
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
&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))."
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.