DSA sheet · Graphs · Dijkstra pattern

Network Delay Time

The second Dijkstra problem. The teacher's message is simple: if you understood the basic Dijkstra video, you can solve this one on your own. She literally copies the previous code and changes a handful of lines. The useful lessons are which lines change and why: the graph is directed, the nodes start at 1, and instead of returning the whole distance list we return its largest value, or −1 if some node can never be reached.

Every problem below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember

Part 0 · Before starting

Directed, weighted graphs

A graph is nodes (circles) joined by edges (lines). Here the edges are directed: an edge u → v is a one-way road. You can go from u to v, but not back from v to u unless a separate edge v → u is given. Each edge also has a weight, here a travel time.

        1
  (2) ———→ (1)        2 can send to 1 in 1 unit of time,
                      but 1 cannot send back to 2

Building the adjacency list (one direction only)

The input is an edge list times of [u, v, w]. We turn it into an adjacency list: for each node, the list of (next node, time) it can send to. For a directed graph we add the edge only from u. (In the undirected Dijkstra problem we added both directions.)

directed adjacency list, nodes 1..n
n = 4
times = [[2, 1, 1], [2, 3, 1], [3, 4, 1]]

graph = [[] for _ in range(n + 1)]   # index 0 unused, nodes are 1..n
for u, v, w in times:
    graph[u].append((v, w))          # one way only: u -> v

print(graph)   # [[], [], [(1, 1), (3, 1)], [(4, 1)], []]
Doubt: why size n + 1?
→ The nodes are labelled 1 to n, not 0 to n−1. A list of size n has indexes 0..n−1, so graph[n] would crash. With size n + 1 the indexes are 0..n; we simply never use index 0. This is easier than subtracting 1 everywhere.

Visited, BFS and why we need Dijkstra here

In unweighted graphs a BFS with a visited array gives shortest distances, because every edge costs the same. Here edges have different times, so the route with fewer edges is not always the faster one. That's why we use Dijkstra: BFS with a min-heap instead of a queue, and "go again only if the new time is smaller" instead of a visited array. Full explanation, including a picture of plain BFS going wrong, is on the Dijkstra page.

The Dijkstra code we start from

recap: Dijkstra from the previous problem (undirected, nodes 0..V-1)
import heapq

def dijkstra(V, edges, src):
    graph = [[] for _ in range(V)]
    for u, v, w in edges:
        graph[u].append((v, w))
        graph[v].append((u, w))
    dist = [float('inf')] * V
    dist[src] = 0
    pq = [(0, src)]                         # (distance, node), smallest first
    while pq:
        d, node = heapq.heappop(pq)
        if d > dist[node]:                  # stale entry
            continue
        for nei, w in graph[node]:
            if d + w < dist[nei]:           # better route found
                dist[nei] = d + w
                heapq.heappush(pq, (d + w, nei))
    return dist

print(dijkstra(3, [[0, 1, 1], [1, 2, 3], [0, 2, 6]], 2))   # [4, 3, 0]

Part A · Dijkstra, then take the maximum

LeetCode 743

1The question in simple words

There are n computers (nodes) labelled 1 to n. times[i] = [u, v, w] means a signal sent from u reaches v after w units of time. The edges are one way. We send a signal from node k. It spreads along every edge at the same time. Return how long it takes until every node has received it. If some node never gets it, return −1.

Her example (LeetCode example 1): times = [[2,1,1],[2,3,1],[3,4,1]], n = 4, k = 2.

            1
     (2) ———————→ (1)
      |
     1|
      ↓       1
     (3) ———————→ (4)
timewho has the signal
02 (the sender)
12, 1, 3
22, 1, 3, 4 → everyone

Answer: 2.

k is just the source under a different name. As she keeps reminding us, a graph has no built-in starting point like a tree's root or a list's head, so the question has to name one.

2What the constraints tell us

3Intuition

The signal travels down every edge at once, like water poured into pipes. Each node gets the signal at the earliest moment any route reaches it, which is exactly its shortest distance from k. So:

4Building the logic from her example

Why "minimum time" becomes a maximum

The words are confusing at first: the question asks for the minimum time, but we return a maximum. She works it out on the example: after 0 units only node 2 has it; after 1 unit nodes 1 and 3 have it, but 4 doesn't; after 2 units all four have it. The earliest moment when all nodes are covered is the arrival time of the slowest node.

Her dist array for the example (index 0 unused): [–, 1, 0, 1, 2] → the max of indexes 1..4 is 2.

When is the answer −1?

She changes the picture: suppose node 4 had no edge coming into it. Then nothing can ever reach it and its dist stays at the starting "infinity". So while we scan for the maximum, if we see an infinity, we return −1 at once.

  (2) ——1——→ (1)
   |
   1                  (4)   ← no edge comes in
   ↓
  (3)

  dist = [–, 1, 0, 1, ∞]  → some node unreached → −1

LeetCode example 3 shows the same thing from the other side: times = [[1,2,1]], n = 2, k = 2. The only edge goes from 1 to 2. Starting at 2 there is no way back to 1 (one-way road), so the answer is −1.

Doubt: why does dist start at infinity again?
→ Same reason as in Dijkstra: every update keeps the smaller value, so the first real time must beat the starting value. Infinity loses to everything. As a bonus, here it also marks the unreached nodes: any slot still at infinity after the loop was never reached.
Doubt: can I just write max(dist)?
→ Not on the whole list. Slot 0 is not a real node and stays infinity, so max(dist) would always be infinity. Look at dist[1:] only, which is what her loop from 1 does.

What changes from the previous code (her "copy and edit" list)

#basic DijkstraNetwork Delay Timewhy
1edges, V, srctimes, n, kjust new names
2add edge both waysadd u → v onlydirected graph
3lists of size Vsize n + 1nodes are 1..n
4return the dist listreturn max of dist[1..n], or −1one number is asked

Everything else (heap of (time, node), stale check, relax if smaller) is unchanged.

Her bug on screenHer first run crashed with "index 4 out of bounds". Two causes, both from the 1-based labels: the list was sized n instead of n + 1, and the final loop stopped at i < n, so it never looked at node n. She fixed both: size n + 1, loop 1 ≤ i ≤ n. In Python that's range(1, n + 1).

5Approach steps

  1. Build a directed adjacency list of size n + 1 from times.
  2. dist = [inf] * (n + 1), dist[k] = 0, heap = [(0, k)].
  3. Run Dijkstra: pop the smallest, skip stale entries, relax each outgoing edge.
  4. Set ans = 0. For each node i from 1 to n: if dist[i] is infinity, return −1; otherwise ans = max(ans, dist[i]).
  5. Return ans.

6Code (Python)

Network Delay Time with Dijkstra
import heapq

class Solution:
    def networkDelayTime(self, times, n, k):
        graph = [[] for _ in range(n + 1)]      # nodes 1..n
        for u, v, w in times:
            graph[u].append((v, w))             # directed: u -> v only

        dist = [float('inf')] * (n + 1)
        dist[k] = 0
        pq = [(0, k)]                           # (time, node)

        while pq:
            t, node = heapq.heappop(pq)
            if t > dist[node]:                  # stale entry
                continue
            for nei, w in graph[node]:
                nt = t + w
                if nt < dist[nei]:
                    dist[nei] = nt
                    heapq.heappush(pq, (nt, nei))

        max_time = 0
        for i in range(1, n + 1):               # skip index 0, include n
            if dist[i] == float('inf'):         # never reached
                return -1
            max_time = max(max_time, dist[i])
        return max_time

7Code line by line

linewhat it means
graph = [[] for _ in range(n + 1)]One list per node; slot 0 unused because labels start at 1.
graph[u].append((v, w))Only u can send to v. No reverse edge.
dist = [float('inf')] * (n + 1) dist[k] = 0Nobody has the signal yet, except k, which has it at time 0.
pq = [(0, k)]Heap ordered by time (time is first in the tuple).
t, node = heapq.heappop(pq)The node that gets the signal earliest among the waiting ones.
if t > dist[node]: continueAn older, slower offer for a node we already reached faster. Skip.
nt = t + w if nt < dist[nei]:Arrival time at nei through this node. Keep it only if it's earlier than what we knew.
dist[nei] = nt heapq.heappush(pq, (nt, nei))Record it and let nei pass the signal on later.
max_time = 0Times are never negative, so 0 is a safe start for the running maximum.
for i in range(1, n + 1):Real nodes only: 1, 2, …, n.
if dist[i] == float('inf'): return -1This node never got the signal, so "everyone receives it" is impossible.
max_time = max(max_time, dist[i])The slowest node decides when everyone is done.

8Dry run: heap and dist after every pop

Run 1: her example (answer 2)

Adjacency: 2: (1,1) (3,1) · 3: (4,1). dist written as [node1, node2, node3, node4]. Start: dist = [∞, 0, ∞, ∞], heap = [(0,2)].

poppopped (t, node)edges checkedheap AFTERdist AFTER (1..4)
1(0, 2)→1: 0+1=1 < ∞ ✓ push
→3: 0+1=1 < ∞ ✓ push
(1,1) (1,3)[1, 0, 1, ∞]
2(1, 1)node 1 has no outgoing edges(1,3)[1, 0, 1, ∞]
3(1, 3)→4: 1+1=2 < ∞ ✓ push(2,4)[1, 0, 1, 2]
4(2, 4)no outgoing edgesempty[1, 0, 1, 2]

Scan 1..4: no infinity; max(1, 0, 1, 2) = 2 ✓.

On a tie like (1,1) and (1,3), Python compares the second item, so node 1 comes out first. Either order gives the same answer.

Run 2: an example with a better route found later (and a stale entry)

times = [[1,2,4],[1,3,1],[3,2,1],[2,4,1]], n = 4, k = 1.

           4
   (1) —————————→ (2) ——1——→ (4)
     \            ↑
     1\          /1
       ↘        /
          (3)

Start: dist = [0, ∞, ∞, ∞], heap = [(0,1)].

poppoppededges checkedheap AFTERdist AFTER (1..4)note
1(0, 1)→2: 4 < ∞ ✓
→3: 1 < ∞ ✓
(1,3) (4,2)[0, 4, 1, ∞]direct road gives 2 a first guess of 4
2(1, 3)→2: 1+1=2 < 4 ✓(2,2) (4,2)[0, 2, 1, ∞]going via 3 is faster; node 2 is in the heap twice
3(2, 2)→4: 2+1=3 < ∞ ✓(3,4) (4,2)[0, 2, 1, 3]
4(3, 4)no outgoing edges(4,2)[0, 2, 1, 3]
5(4, 2)4 > dist[2] = 2 → stale, skipempty[0, 2, 1, 3]the old slow offer

Scan: max(0, 2, 1, 3) = 3. Without the stale skip, pop 5 would recheck node 2's edge to 4 with time 5, fail the < test, and waste a little work. The answer would still be 3.

Run 3: unreachable node (answer −1)

times = [[1,2,1],[2,3,1]], n = 4, k = 1. Pops: (0,1) → dist[2] = 1; (1,2) → dist[3] = 2; (2,3) → no edges; heap empty. dist (1..4) = [0, 1, 2, ∞]. The scan hits node 4 = ∞ → −1.

9Complexity & remember

Remember Network Delay TimeDijkstra from k (directed, nodes 1..n, size n + 1) → answer = max of dist[1..n] → any ∞ means −1.
"Minimum time for everyone" = the slowest node's fastest arrival.

Part B · Revision page

Dijkstra (problem 25)Network Delay Time
graphundirecteddirected
node labels0..V−11..n → size n + 1
sourcesrck
returnthe whole dist listmax(dist[1..n]), or −1 if any ∞
core loopidentical: heap of (dist, node), skip stale, relax if smaller
time / spaceO((V + E) log V) / O(V + E)
If you remember only 5 lines 1. Build the graph one way only: graph[u].append((v, w)).
2. Size n + 1 because nodes are 1..n.
3. Normal Dijkstra from k.
4. Any dist[i] still ∞ for i in 1..n → return −1.
5. Else return the max of dist[1..n].
Mistakes to avoid ✗ adding the reverse edge (the graph is directed)
✗ size n instead of n + 1 (index out of range, her bug)
✗ range(1, n), which forgets node n (her other bug)
✗ max(dist) including slot 0, which is always ∞
✗ returning the sum of times instead of the max (signals travel in parallel)
test it yourself (paste under the solution)
s = Solution()
print(s.networkDelayTime([[2, 1, 1], [2, 3, 1], [3, 4, 1]], 4, 2))             # 2
print(s.networkDelayTime([[1, 2, 1]], 2, 1))                                  # 1
print(s.networkDelayTime([[1, 2, 1]], 2, 2))                                  # -1
print(s.networkDelayTime([[1, 2, 4], [1, 3, 1], [3, 2, 1], [2, 4, 1]], 4, 1)) # 3
print(s.networkDelayTime([], 1, 1))                                           # 0

Based on this video: Network Delay Time