Coding Interview Patterns

Course Content

Coding Interview Patterns

20 sections · 146 lessons

Network Delay Time


Once edges carry weights, BFS stops being correct. It counts edges, not cost. Take three nodes: A to C costs 5, while A to B and B to C cost 1 each. BFS reaches C in one hop and reports 5, but A → B → C costs 2. Dijkstra's algorithm is the repair: expand nodes in order of total cost, not in order of hops.

The problem

A network has n nodes, numbered 1 to n. You are given directed edges [u, v, t]: a signal sent from u reaches v after t time units. A signal starts at node k. Return the time until every node has received it. If some node never receives it, return -1.

Example 1. n = 4, k = 1, edges [[1, 2, 1], [1, 3, 4], [2, 3, 2], [2, 4, 6], [3, 4, 3]] → 6. The fastest times are: node 2 at 1; node 3 at 3 (via 2, cheaper than the direct 4); node 4 at 6 (via 2 and 3, cheaper than 1 + 6 = 7). The last node hears the signal at time 6.

Example 2. n = 3, k = 1, edges [[1, 2, 5]] → -1. Node 3 has no incoming edge.

Constraints. Up to 10^4 nodes and 10^5 edges. Times are integers from 0 to 100.

Clarifying questions

  • Directed? Yes: u → v does not imply v → u.
  • Can times be negative? No — this decides the algorithm, so always ask.
  • Can there be several edges between the same pair? Assume not; the solution handles it anyway.
  • Is the answer "all nodes heard it"? Yes: the maximum of all shortest times.

Approach 1: the simple way — Bellman-Ford

The reframe comes first: "when does the last node hear it?" is "the largest of all the shortest times". Now compute shortest times the simplest way. Start with 0 at k and infinity elsewhere. Go through every edge and relax it: if reaching u and then taking u → v is faster than v's current time, lower v's time. Repeat the pass. A shortest path uses at most n − 1 edges, and pass i fixes every shortest path of i edges, so n − 1 passes are enough.

Python
def network_delay_bellman_ford(times: list[list[int]], n: int, k: int) -> int:    """Relax every edge up to n-1 times."""    dist = [float("inf")] * (n + 1)    dist[k] = 0    for _ in range(n - 1):        changed = False        for u, v, w in times:            if dist[u] + w < dist[v]:                dist[v] = dist[u] + w                changed = True        if not changed:            break                                # nothing moved: already final    longest = max(dist[1:])    return -1 if longest == float("inf") else int(longest)

On Example 1 the edges happen to be listed in a helpful order, so pass 1 already gives [0, 1, 3, 6] and pass 2 changes nothing and stops. In the worst case, though, it needs all n − 1 passes over all E edges: O(V × E). With 10^4 nodes and 10^5 edges that is 10^9 relaxations. The waste: every pass re-relaxes edges out of nodes whose times are already final.

The key insight

Process nodes in order of their time, smallest first. When the node with the smallest known time is taken, its time is final. Any other route to it must pass through some node that is not yet final, whose time is already at least as large — and since no edge has a negative time, continuing from there can only add. So each node needs to be expanded once, at the moment it is the closest unfinished node.

Finding "the smallest known time" quickly is a job for a min-heap. That is Dijkstra's algorithm: BFS with the queue replaced by a priority queue keyed by total time.

The whole argument rests on one condition: no negative edges. Say it out loud before you write the code.

Approach 2: Dijkstra

Python
import heapqdef network_delay_time(times: list[list[int]], n: int, k: int) -> int:    """Dijkstra from k; the answer is the largest shortest distance."""    graph: list[list[tuple[int, int]]] = [[] for _ in range(n + 1)]    for u, v, w in times:        graph[u].append((v, w))    dist = [float("inf")] * (n + 1)    dist[k] = 0    heap = [(0, k)]                              # (distance so far, node)    while heap:        d, u = heapq.heappop(heap)        if d > dist[u]:            continue                             # stale entry: u already settled        for v, w in graph[u]:            if d + w < dist[v]:                  # relax the edge                dist[v] = d + w                heapq.heappush(heap, (dist[v], v))    longest = max(dist[1:])    return -1 if longest == float("inf") else int(longest)

Step by step:

  1. Build an adjacency list of (neighbour, time) pairs.
  2. Push (0, k). Pop the smallest entry each time.
  3. Skip stale entries. Python's heapq cannot lower a key in place, so when a node's time improves, a new entry is pushed and the old one stays behind. When an old entry is popped later, its time is larger than dist[u]; skip it.
  4. Relax each outgoing edge; if a time improves, record it and push the new entry.
  5. The answer is the largest time among nodes 1 to n, or -1 if any is still infinite.

Dry run on Example 1. The heap is shown sorted.

stepheap beforepoppedstale?relaxationsdist[1..4] after
1(0,1)(0,1)no2: 0+1 = 1; 3: 0+4 = 40, 1, 4, ∞
2(1,2), (4,3)(1,2)no3: 1+2 = 3, beats 4; 4: 1+6 = 70, 1, 3, 7
3(3,3), (4,3), (7,4)(3,3)no4: 3+3 = 6, beats 70, 1, 3, 6
4(4,3), (6,4), (7,4)(4,3)yes: 4 is more than 3skippedunchanged
5(6,4), (7,4)(6,4)nonone: node 4 has no edgesunchanged
6(7,4)(7,4)yes: 7 is more than 6skippedunchanged

Final times: 0, 1, 3, 6. The largest is 6. Node 3's time was set to 4 in step 1 and improved in step 2 — that is relaxation working, and why the same node can sit in the heap twice.

42518102A0B3C2D8E10startpriority queuepop A (0)B ← 4 via AC ← 2 via Apop C (2)B ← 3 via C (beats 4)D ← 10, E ← 12pop B (3)D ← 8 via B (beats 10)pop D (8)E ← 10 via D (beats 12)pop E (10)queue empty — doneThe bold path A → C → B → D → E costs 10. A greedy first hop to B costs 4 and loses: the cheapest first edgeis not the cheapest path.Each node's number is its final distance. B is relaxed twice — 4, then 3 — which is why Dijkstra needs apriority queue rather than a plain queue.
B is improved from 4 to 3 after C is popped — settling nodes in distance order is what makes that safe.

Complexity. Each edge can cause at most one push, so the heap holds at most E entries, and each push or pop costs O(log E) = O(log V). Total O(E log V) time, O(V + E) space. For 10^5 edges that is about 1.7 × 10^6 heap steps, against 10^9 for Bellman-Ford's worst case.

Edge cases

  • An unreachable node (Example 2) → its time stays infinite: -1.
  • n = 1 → the signal is already everywhere: 0.
  • Zero-time edges → allowed; Dijkstra needs non-negative, not positive.
  • Two edges between the same pair → both are relaxed; the cheaper wins.
  • Nodes numbered from 1 → arrays of size n + 1, and take the maximum over dist[1:] so the unused slot 0 (always infinite) does not turn every answer into -1.

Follow-ups: when Dijkstra is not enough

The two things that break DijkstraNegative edges• A node is finalised too early• Bellman-Ford relaxes n minus 1 times• A change on pass nmeans a negative cycleA second constraint• Cheapest flight within K stops• Cost alone no longer defines the state• Relax exactly K plus 1 rounds
Dijkstra assumes an extra edge never lowers a cost — negative weights break exactly that assumption.
  • "Some times can be negative." Dijkstra's promise that a popped node is final breaks. With A → B = 2, A → C = 5 and C → B = −4, a version that never reopens a finished node reports 2 for B, though A → C → B costs 1. Use Bellman-Ford (Approach 1). One extra pass also detects a negative cycle: if anything still improves after n − 1 passes, times can fall forever and no shortest path exists.
  • "Cheapest route using at most K stops." Dijkstra keeps one value per node, but here a pricier route with fewer stops can be the one that finishes cheaper. Run Bellman-Ford for exactly K + 1 passes, each reading only the previous pass's prices:
Python
def cheapest_within_k_stops(n: int, flights: list[list[int]], src: int, dst: int, k: int) -> int:    """Follow-up: at most k stops = at most k+1 flights = k+1 rounds of relaxing."""    cost = [float("inf")] * n    cost[src] = 0    for _ in range(k + 1):        prev = cost[:]                           # read only last round's costs        for u, v, price in flights:            if prev[u] + price < cost[v]:                cost[v] = prev[u] + price    return -1 if cost[dst] == float("inf") else int(cost[dst])

The copy is the whole trick: relaxing against the live array could chain two flights in one pass and quietly break the stop limit. This costs O(K × E).

  • "All weights are equal." Use plain BFS: O(V + E), no heap.
situationalgorithmcost
unweighted, shortest pathBFSO(V + E)
non-negative weightsDijkstraO(E log V)
negative weights possibleBellman-FordO(V × E)
a limit on the number of edgesBellman-Ford, K + 1 passesO(K × E)

Check your understanding

0 of 2 answered

1.Why is a node's time final when Dijkstra pops it from the heap?

2.An interviewer adds one edge with time −3. What should you say?