743. Network Delay Time - Explanation

Problem Link

Description

You are given a network of n directed nodes, labeled from 1 to n. You are also given times, a list of directed edges where times[i] = (ui, vi, ti).

  • ui is the source node (an integer from 1 to n)
  • vi is the target node (an integer from 1 to n)
  • ti is the time it takes for a signal to travel from the source to the target node (an integer greater than or equal to 0).

You are also given an integer k, representing the node that we will send a signal from.

Return the minimum time it takes for all of the n nodes to receive the signal. If it is impossible for all the nodes to receive the signal, return -1 instead.

Example 1:

Input: times = [[1,2,1],[2,3,1],[1,4,4],[3,4,1]], n = 4, k = 1

Output: 3

Example 2:

Input: times = [[1,2,1],[2,3,1]], n = 3, k = 2

Output: -1

Constraints:

  • 1 <= k <= n <= 100
  • 1 <= times.length <= 1000


Topics

Recommended Time & Space Complexity

You should aim for a solution as good or better than O(ElogV) time and O(V + E) space, where E is the number of edges and V is the number of vertices (nodes).


Hint 1

As we are given the source node and we need to find the minimum time to reach all nodes, this represents the shortest path from the source node to all nodes. Can you think of a standard algorithm to find the shortest path from a source to a destination? Maybe a heap-based algorithm is helpful.


Hint 2

We can use Dijkstra's algorithm to find the shortest path from a source to destination. We end up finding the shortest paths from the source to the nodes that we encounter in finding the destination. So, to find shortest path for all nodes from the source, we need to perform Dijkstra's algorithm until the heap in this algorithm becomes empty. How would you implement this?


Hint 3

We use a Min-Heap as we need to find the minimum time. We create an adjacency list for the given times (weighted edges). We also initialize an array dist[] of size n (number of nodes) which represents the distance from the source to all nodes, initialized with infinity. We put dist[source] = 0. Then we continue the algorithm. After the heap becomes empty, if we don't visit any node, we return -1; otherwise, we return the time.


Company Tags


Prerequisites

Before attempting this problem, you should be comfortable with:

  • Graph Representation (Adjacency List) - Building and traversing weighted directed graphs
  • Depth First Search (DFS) - Recursive graph traversal with path tracking
  • Shortest Path Algorithms - Understanding Dijkstra's, Bellman-Ford, and Floyd-Warshall concepts
  • Priority Queue / Min-Heap - Efficiently extracting minimum elements for Dijkstra's algorithm
  • Hash Maps - Storing distances and graph edges for quick lookups

Intuition

We want to know how long it takes for a signal to reach all nodes starting from node k.

Using DFS, we try all possible paths from k and keep track of the minimum time needed to reach each node.

  • If we reach a node with a better (smaller) time, we update it.
  • If the current path is already worse than a known path, we stop exploring it (pruning).

After exploring all reachable paths:

  • The answer is the maximum time among all nodes (last node to receive the signal).
  • If any node is unreachable, return -1.

Algorithm

  1. Build a graph from times where each edge has a weight.
  2. Initialize a distance array with for all nodes.
  3. Start dfs from node k with time 0.
  4. During dfs:
    • If current time ≥ known shortest time, stop.
    • Otherwise, update the shortest time and continue dfs to neighbors.
  5. After dfs:
    • Take the maximum distance.
    • If any distance is , return -1, else return the maximum.
class Solution:
    def networkDelayTime(self, times: List[List[int]], n: int, k: int) -> int:
        adj = defaultdict(list)
        for u, v, w in times:
            adj[u].append((v, w))

        dist = {node: float("inf") for node in range(1, n + 1)}

        def dfs(node, time):
            if time >= dist[node]:
                return

            dist[node] = time
            for nei, w in adj[node]:
                dfs(nei, time + w)

        dfs(k, 0)
        res = max(dist.values())
        return res if res < float('inf') else -1

Time & Space Complexity

  • Time complexity: O(VE)O(V * E)
  • Space complexity: O(V+E)O(V + E)

Where VV is the number of vertices and EE is the number of edges.


2. Floyd Warshall Algorithm

Intuition

We want the shortest time between every pair of nodes so that we can easily know how long it takes for the signal to reach all nodes starting from k.

Floyd–Warshall is an all-pairs shortest path algorithm:

  • It repeatedly tries to improve the shortest path between every (i, j) by allowing an intermediate node mid.
  • After processing all intermediates, dist[i][j] stores the shortest time from i to j.

Once all shortest paths are known:

  • Look at the row corresponding to the starting node k.
  • The maximum value in that row is the time when the last node receives the signal.
  • If any node is unreachable (distance = ∞), return -1.

Algorithm

  1. Create a dist matrix initialized with .
  2. Set dist[u][v] = w for every directed edge (u → v) with weight w.
  3. Set dist[i][i] = 0 for all nodes.
  4. For each node mid:
    • For every pair (i, j), update
      dist[i][j] = min(dist[i][j], dist[i][mid] + dist[mid][j])
  5. Take the maximum distance from node k to all nodes.
  6. If the maximum is , return -1; otherwise return it.
class Solution:
    def networkDelayTime(self, times: List[List[int]], n: int, k: int) -> int:
        inf = float('inf')
        dist = [[inf] * n for _ in range(n)]

        for u, v, w in times:
            dist[u-1][v-1] = w
        for i in range(n):
            dist[i][i] = 0

        for mid in range(n):
            for i in range(n):
                for j in range(n):
                    dist[i][j] = min(dist[i][j], dist[i][mid] + dist[mid][j])

        res = max(dist[k-1])
        return res if res < inf else -1

Time & Space Complexity

  • Time complexity: O(V3)O(V ^ 3)
  • Space complexity: O(V2)O(V ^ 2)

Where VV is the number of vertices.


3. Bellman Ford Algorithm

Intuition

We want the shortest time for a signal to reach every node starting from node k.

Bellman–Ford works by:

  • Relaxing (updating) all edges repeatedly.
  • Each relaxation tries to improve the shortest distance to a node using one more edge.
  • After n - 1 rounds, all shortest paths are guaranteed to be found (because the longest simple path has at most n - 1 edges).

Once distances are finalized:

  • The maximum distance tells us when the last node receives the signal.
  • If any node is still unreachable (), return -1.

Algorithm

  1. Initialize a distance array dist of size n with .
  2. Set dist[k - 1] = 0 (source node).
  3. Repeat n - 1 times:
    • For every edge (u → v, w):
      • If dist[u] + w < dist[v], update dist[v].
  4. Find the maximum value in dist.
  5. If the maximum is , return -1; otherwise return it.
class Solution:
    def networkDelayTime(self, times: List[List[int]], n: int, k: int) -> int:
        dist = [float('inf')] * n
        dist[k - 1] = 0
        for _ in range(n - 1):
            for u, v, w in times:
                if dist[u - 1] + w < dist[v - 1]:
                    dist[v - 1] = dist[u - 1] + w
        max_dist = max(dist)
        return max_dist if max_dist < float('inf') else -1

Time & Space Complexity

  • Time complexity: O(VE)O(V * E)
  • Space complexity: O(V)O(V)

Where VV is the number of vertices and EE is the number of edges.


4. Shortest Path Faster Algorithm

Intuition

SPFA (Shortest Path Faster Algorithm) is an optimized version of Bellman–Ford.

Instead of relaxing all edges every time, we:

  • Only re-process nodes whose distance was actually improved
  • Use a queue to propagate distance updates efficiently

Whenever a node’s shortest time decreases, its neighbors might also get a shorter path — so we push that node into the queue.

This avoids unnecessary work and is usually much faster in practice.

Algorithm

  1. Build an adjacency list from times.
  2. Initialize dist for all nodes as .
  3. Set dist[k] = 0 and push (k, 0) into a queue.
  4. While the queue is not empty:
    • Pop (node, time)
    • If time > dist[node], skip it (outdated path)
    • For each neighbor:
      • If time + weight < dist[neighbor]:
        • Update dist[neighbor]
        • Push (neighbor, newTime) into the queue
  5. Take the maximum value in dist.
  6. If any node is unreachable (), return -1; otherwise return the max time.
class Solution:
    def networkDelayTime(self, times, n, k):
        adj = defaultdict(list)
        for u, v, w in times:
            adj[u].append((v, w))

        dist = {node: float("inf") for node in range(1, n + 1)}
        q = deque([(k, 0)])
        dist[k] = 0

        while q:
            node, time = q.popleft()
            if dist[node] < time:
                continue
            for nei, w in adj[node]:
                if time + w < dist[nei]:
                    dist[nei] = time + w
                    q.append((nei, time + w))

        res = max(dist.values())
        return res if res < float('inf') else -1

Time & Space Complexity

  • Time complexity: O(V+E)O(V + E) in average case, O(VE)O(V * E) in worst case.
  • Space complexity: O(V+E)O(V + E)

Where VV is the number of vertices and EE is the number of edges.


5. Dijkstra's Algorithm

Intuition

Dijkstra's Algorithm finds the shortest time from the source node k to all other nodes when all edge weights are non-negative.

The key idea:

  • Always expand the node that currently has the smallest known time
  • Once a node is picked from the min-heap, its shortest time is final
  • Use a min-heap (priority queue) to always process the closest node next

By doing this, we gradually spread the signal in increasing order of time.

Algorithm

  1. Build an adjacency list from times.
  2. Use a min-heap initialized with (0, k) (time, node).
  3. Maintain a visited set to avoid reprocessing nodes.
  4. While the heap is not empty:
    • Pop the node with the smallest time.
    • If already visited, skip.
    • Mark it visited and update the current time.
    • Push all unvisited neighbors with updated times into the heap.
  5. If all n nodes are visited, return the maximum time used.
  6. Otherwise, return -1 (some nodes are unreachable).
class Solution:
    def networkDelayTime(self, times: List[List[int]], n: int, k: int) -> int:
        edges = collections.defaultdict(list)
        for u, v, w in times:
            edges[u].append((v, w))

        minHeap = [(0, k)]
        visit = set()
        t = 0
        while minHeap:
            w1, n1 = heapq.heappop(minHeap)
            if n1 in visit:
                continue
            visit.add(n1)
            t = w1

            for n2, w2 in edges[n1]:
                if n2 not in visit:
                    heapq.heappush(minHeap, (w1 + w2, n2))
        return t if len(visit) == n else -1

Time & Space Complexity

  • Time complexity: O(ElogV)O(E \log V)
  • Space complexity: O(V+E)O(V + E)

Where VV is the number of vertices and EE is the number of edges.


Common Pitfalls

Returning Max Instead of Checking Unreachable Nodes

The answer is the maximum time to reach any node, but only if all nodes are reachable. Some solutions return the maximum distance without first checking if any node remains at infinity. This returns an incorrect large value instead of -1 when nodes are unreachable.

Using 1-Based vs 0-Based Indexing

Nodes are numbered from 1 to n, not 0 to n-1. Mixing up indexing when building the adjacency list or distance array causes out-of-bounds errors or skipped nodes. Consistently use either 1-based arrays of size n+1 or subtract 1 from all node numbers.

Revisiting Nodes Without Proper Checks

In Dijkstra's algorithm, once a node is finalized (popped from the min-heap), its shortest distance is determined. Processing the same node again wastes time and can cause issues in some implementations. Always skip nodes that have already been visited or whose current distance exceeds the known shortest.