Facebook Pixel

2642. Design Graph With Shortest Path Calculator

Problem Description

You need to implement a [Graph](/problems/graph_intro) class that represents a directed weighted graph with n nodes numbered from 0 to n - 1.

The class should support three operations:

  1. Constructor [Graph](/problems/graph_intro)(int n, int[][] edges): Initialize the graph with n nodes and a list of edges. Each edge is represented as [from, to, edgeCost], meaning there's a directed edge from node from to node to with weight edgeCost.

  2. Add Edge addEdge(int[] edge): Add a new edge to the graph. The edge is given as [from, to, edgeCost]. It's guaranteed that this edge doesn't already exist in the graph.

  3. Find Shortest Path shortestPath(int node1, int node2): Calculate and return the minimum cost to travel from node1 to node2. The cost of a path is the sum of all edge weights along that path. If no path exists from node1 to node2, return -1.

The solution uses Dijkstra's algorithm to find the shortest path. The graph is stored as an adjacency matrix where g[i][j] represents the edge weight from node i to node j. If there's no direct edge between two nodes, the weight is set to infinity. During the shortest path calculation, the algorithm maintains a distance array dist where dist[i] stores the shortest distance from the source node to node i, and a visited array vis to track which nodes have been processed. The algorithm iteratively selects the unvisited node with the minimum distance, marks it as visited, and updates the distances to its neighbors.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

When we need to find the shortest path in a weighted graph, we naturally think about classic shortest path algorithms. Since we're dealing with a single-source shortest path problem (finding the shortest path from one specific node to another), Dijkstra's algorithm is a natural choice.

The key insight is that we need a data structure that allows efficient edge additions and can quickly compute shortest paths when needed. An adjacency matrix g[i][j] is ideal here because:

  • Adding an edge is simply updating one cell in the matrix: g[from][to] = cost
  • We can easily iterate through all neighbors of a node by checking a row in the matrix

For the shortest path computation, we follow the greedy principle of Dijkstra's algorithm: at each step, we extend our shortest path tree by choosing the unvisited node that's closest to the source. This works because once we visit a node with the minimum distance, we know we've found the shortest path to it (assuming non-negative edge weights).

The algorithm maintains two key pieces of information:

  • dist[i]: The shortest known distance from the source to node i
  • vis[i]: Whether we've confirmed the shortest path to node i

We start by setting dist[node1] = 0 (distance to source is 0) and all other distances to infinity. Then, we repeatedly:

  1. Pick the unvisited node t with the smallest distance
  2. Mark t as visited (we've found its shortest path)
  3. Update distances to all neighbors of t using the relaxation formula: dist[j] = min(dist[j], dist[t] + g[t][j])

This process continues for n iterations (once for each node), ensuring we explore all possible paths. If dist[node2] remains infinity after the algorithm completes, it means node2 is unreachable from node1.

Learn more about Graph, Shortest Path and Heap (Priority Queue) patterns.

Solution Approach

The implementation uses an adjacency matrix representation with Dijkstra's algorithm for finding shortest paths.

Initialization (__init__):

  • Create an n × n adjacency matrix g where all values are initialized to inf (infinity)
  • For each edge [f, t, c] in the input edges list, set g[f][t] = c to represent a directed edge from node f to node t with cost c
  • This matrix representation allows O(1) edge lookups and updates

Adding Edges (addEdge):

  • Extract the edge components: f, t, c = edge (from, to, cost)
  • Simply update the adjacency matrix: g[f][t] = c
  • This operation takes O(1) time

Finding Shortest Path (shortestPath):

The implementation follows Dijkstra's algorithm with a linear search for the minimum distance node:

  1. Initialize distance array: Create dist array of size n with all values set to inf, except dist[node1] = 0 (source node has distance 0)

  2. Initialize visited array: Create vis array of size n with all values set to False

  3. Main algorithm loop: Repeat n times:

    • Find minimum unvisited node:

      t = -1
      for j in range(self.n):
          if not vis[j] and (t == -1 or dist[t] > dist[j]):
              t = j

      This finds the unvisited node t with the smallest distance value

    • Mark as visited: Set vis[t] = True to indicate we've found the shortest path to node t

    • Relax edges: Update distances to all neighbors of t:

      for j in range(self.n):
          dist[j] = min(dist[j], dist[t] + self.g[t][j])

      If going through node t provides a shorter path to node j, update dist[j]

  4. Return result: After processing all nodes, check if dist[node2] == inf. If so, return -1 (no path exists). Otherwise, return dist[node2] (the shortest path distance)

Time Complexity:

  • __init__: O(n² + E) where E is the number of initial edges
  • addEdge: O(1)
  • shortestPath: O(n²) due to the nested loops - finding minimum node takes O(n) and we do this n times

Space Complexity: O(n²) for storing the adjacency matrix

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a small example to illustrate how the solution works.

Example Setup:

  • Create a graph with 4 nodes (0, 1, 2, 3)
  • Initial edges: [[0,1,2], [1,2,3], [0,3,6]]
  • Find shortest path from node 0 to node 2
  • Add edge [3,2,1]
  • Find shortest path from node 0 to node 2 again

Step 1: Initialize the Graph

Graph(4, [[0,1,2], [1,2,3], [0,3,6]])

Creates adjacency matrix g:

     0   1   2   3
0 [ inf  2  inf  6 ]
1 [ inf inf  3  inf]
2 [ inf inf inf inf]
3 [ inf inf inf inf]

Step 2: Find Shortest Path from 0 to 2

Initialize:

  • dist = [0, inf, inf, inf] (distance from node 0)
  • vis = [False, False, False, False]

Iteration 1:

  • Find minimum unvisited: node 0 (dist=0)
  • Mark node 0 as visited: vis = [True, False, False, False]
  • Relax edges from node 0:
    • dist[1] = min(inf, 0+2) = 2
    • dist[3] = min(inf, 0+6) = 6
  • Updated dist = [0, 2, inf, 6]

Iteration 2:

  • Find minimum unvisited: node 1 (dist=2)
  • Mark node 1 as visited: vis = [True, True, False, False]
  • Relax edges from node 1:
    • dist[2] = min(inf, 2+3) = 5
  • Updated dist = [0, 2, 5, 6]

Iteration 3:

  • Find minimum unvisited: node 2 (dist=5)
  • Mark node 2 as visited: vis = [True, True, True, False]
  • Relax edges from node 2: (no outgoing edges)
  • dist remains [0, 2, 5, 6]

Iteration 4:

  • Find minimum unvisited: node 3 (dist=6)
  • Mark node 3 as visited: vis = [True, True, True, True]
  • Relax edges from node 3: (no outgoing edges initially)
  • Final dist = [0, 2, 5, 6]

Result: Shortest path from 0 to 2 = 5 (path: 0→1→2)

Step 3: Add Edge [3,2,1]

addEdge([3,2,1])

Updates adjacency matrix:

     0   1   2   3
0 [ inf  2  inf  6 ]
1 [ inf inf  3  inf]
2 [ inf inf inf inf]
3 [ inf inf  1  inf]  ← New edge added

Step 4: Find Shortest Path from 0 to 2 Again

Initialize:

  • dist = [0, inf, inf, inf]
  • vis = [False, False, False, False]

Following the same process:

  • After processing node 0: dist = [0, 2, inf, 6]
  • After processing node 1: dist = [0, 2, 5, 6]
  • After processing node 2: dist = [0, 2, 5, 6]
  • After processing node 3: dist[2] = min(5, 6+1) = 5

Wait, let's recalculate more carefully:

Iteration 1: Process node 0

  • dist = [0, 2, inf, 6]

Iteration 2: Process node 1 (dist=2)

  • dist = [0, 2, 5, 6]

Iteration 3: Process node 2 (dist=5)

  • No change

Iteration 4: Process node 3 (dist=6)

  • dist[2] = min(5, 6+1) = 5 (no improvement)

Actually, we need to process in the correct order. Let me redo:

Iteration 1: Process node 0

  • Relax: dist = [0, 2, inf, 6]

Iteration 2: Process node 1 (smallest unvisited with dist=2)

  • Relax: dist = [0, 2, 5, 6]

Iteration 3: Process node 2 (smallest unvisited with dist=5)

  • No outgoing edges, no change

Iteration 4: Process node 3 (smallest unvisited with dist=6)

  • Relax: dist[2] = min(5, 6+1) = 5 (no change, but would be 7 if we took path 0→3→2)

Result: Shortest path from 0 to 2 = 5 (still path: 0→1→2, the new edge didn't provide a shorter path)

This example demonstrates how the algorithm explores all possible paths and always finds the optimal solution, even when new edges are added to the graph.

Solution Implementation

1from typing import List
2from math import inf
3
4
5class Graph:
6    def __init__(self, n: int, edges: List[List[int]]):
7        """
8        Initialize the graph with n nodes and given edges.
9      
10        Args:
11            n: Number of nodes in the graph (0 to n-1)
12            edges: List of edges where each edge is [from_node, to_node, cost]
13        """
14        self.n = n
15        # Initialize adjacency matrix with infinity (no direct connection)
16        self.adjacency_matrix = [[inf] * n for _ in range(n)]
17      
18        # Populate the adjacency matrix with given edges
19        for from_node, to_node, cost in edges:
20            self.adjacency_matrix[from_node][to_node] = cost
21
22    def addEdge(self, edge: List[int]) -> None:
23        """
24        Add a new edge to the graph or update existing edge cost.
25      
26        Args:
27            edge: Edge to add as [from_node, to_node, cost]
28        """
29        from_node, to_node, cost = edge
30        self.adjacency_matrix[from_node][to_node] = cost
31
32    def shortestPath(self, node1: int, node2: int) -> int:
33        """
34        Find the shortest path from node1 to node2 using Dijkstra's algorithm.
35      
36        Args:
37            node1: Starting node
38            node2: Destination node
39          
40        Returns:
41            Shortest distance from node1 to node2, or -1 if no path exists
42        """
43        # Initialize distances from source node to all nodes
44        distances = [inf] * self.n
45        distances[node1] = 0
46      
47        # Track visited nodes
48        visited = [False] * self.n
49      
50        # Process all nodes
51        for _ in range(self.n):
52            # Find the unvisited node with minimum distance
53            current_node = -1
54            for node in range(self.n):
55                if not visited[node] and (current_node == -1 or distances[current_node] > distances[node]):
56                    current_node = node
57          
58            # Mark current node as visited
59            visited[current_node] = True
60          
61            # Update distances to all neighbors of current node
62            for neighbor in range(self.n):
63                distances[neighbor] = min(distances[neighbor], 
64                                         distances[current_node] + self.adjacency_matrix[current_node][neighbor])
65      
66        # Return the shortest distance to destination (or -1 if unreachable)
67        return -1 if distances[node2] == inf else distances[node2]
68
69
70# Your Graph object will be instantiated and called as such:
71# obj = Graph(n, edges)
72# obj.addEdge(edge)
73# param_2 = obj.shortestPath(node1, node2)
74
1class Graph {
2    private int numberOfNodes;
3    private int[][] adjacencyMatrix;
4    private static final int INFINITY = 1 << 29;  // Large value representing infinity (2^29)
5
6    /**
7     * Constructor to initialize the graph with n nodes and given edges
8     * @param n Number of nodes in the graph
9     * @param edges Array of edges where each edge is [from, to, cost]
10     */
11    public Graph(int n, int[][] edges) {
12        this.numberOfNodes = n;
13        this.adjacencyMatrix = new int[n][n];
14      
15        // Initialize all distances to infinity
16        for (int[] row : adjacencyMatrix) {
17            Arrays.fill(row, INFINITY);
18        }
19      
20        // Add all initial edges to the graph
21        for (int[] edge : edges) {
22            int from = edge[0];
23            int to = edge[1];
24            int cost = edge[2];
25            adjacencyMatrix[from][to] = cost;
26        }
27    }
28
29    /**
30     * Adds a new directed edge to the graph
31     * @param edge Array containing [from, to, cost]
32     */
33    public void addEdge(int[] edge) {
34        int from = edge[0];
35        int to = edge[1];
36        int cost = edge[2];
37        adjacencyMatrix[from][to] = cost;
38    }
39
40    /**
41     * Finds the shortest path from node1 to node2 using Dijkstra's algorithm
42     * @param node1 Starting node
43     * @param node2 Destination node
44     * @return Shortest distance from node1 to node2, or -1 if no path exists
45     */
46    public int shortestPath(int node1, int node2) {
47        // Initialize distance array and visited array
48        int[] distance = new int[numberOfNodes];
49        boolean[] visited = new boolean[numberOfNodes];
50        Arrays.fill(distance, INFINITY);
51        distance[node1] = 0;
52      
53        // Dijkstra's algorithm main loop
54        for (int i = 0; i < numberOfNodes; i++) {
55            // Find unvisited node with minimum distance
56            int currentNode = -1;
57            for (int j = 0; j < numberOfNodes; j++) {
58                if (!visited[j] && (currentNode == -1 || distance[currentNode] > distance[j])) {
59                    currentNode = j;
60                }
61            }
62          
63            // Mark current node as visited
64            visited[currentNode] = true;
65          
66            // Update distances to all neighbors of current node
67            for (int neighbor = 0; neighbor < numberOfNodes; neighbor++) {
68                distance[neighbor] = Math.min(distance[neighbor], 
69                                             distance[currentNode] + adjacencyMatrix[currentNode][neighbor]);
70            }
71        }
72      
73        // Return result: -1 if no path exists, otherwise return the shortest distance
74        return distance[node2] >= INFINITY ? -1 : distance[node2];
75    }
76}
77
78/**
79 * Your Graph object will be instantiated and called as such:
80 * Graph obj = new Graph(n, edges);
81 * obj.addEdge(edge);
82 * int param_2 = obj.shortestPath(node1,node2);
83 */
84
1class Graph {
2public:
3    /**
4     * Initialize the graph with n nodes and given edges
5     * @param n: number of nodes in the graph (0 to n-1)
6     * @param edges: list of edges, each edge is [from, to, cost]
7     */
8    Graph(int n, vector<vector<int>>& edges) {
9        this->nodeCount = n;
10        // Initialize adjacency matrix with infinity (no direct connection)
11        adjacencyMatrix = vector<vector<int>>(n, vector<int>(n, INF));
12      
13        // Add all initial edges to the graph
14        for (auto& edge : edges) {
15            int from = edge[0];
16            int to = edge[1];
17            int cost = edge[2];
18            adjacencyMatrix[from][to] = cost;
19        }
20    }
21  
22    /**
23     * Add a new edge to the graph or update existing edge
24     * @param edge: [from, to, cost]
25     */
26    void addEdge(vector<int> edge) {
27        int from = edge[0];
28        int to = edge[1];
29        int cost = edge[2];
30        adjacencyMatrix[from][to] = cost;
31    }
32  
33    /**
34     * Find shortest path from node1 to node2 using Dijkstra's algorithm
35     * @param node1: starting node
36     * @param node2: destination node
37     * @return: shortest distance from node1 to node2, or -1 if unreachable
38     */
39    int shortestPath(int node1, int node2) {
40        // Track visited nodes
41        vector<bool> visited(nodeCount, false);
42        // Store minimum distance from source to each node
43        vector<int> distance(nodeCount, INF);
44        distance[node1] = 0;
45      
46        // Process all nodes
47        for (int i = 0; i < nodeCount; ++i) {
48            // Find unvisited node with minimum distance
49            int currentNode = -1;
50            for (int j = 0; j < nodeCount; ++j) {
51                if (!visited[j] && (currentNode == -1 || distance[currentNode] > distance[j])) {
52                    currentNode = j;
53                }
54            }
55          
56            // Mark current node as visited
57            visited[currentNode] = true;
58          
59            // Update distances to all neighbors of current node
60            for (int neighbor = 0; neighbor < nodeCount; ++neighbor) {
61                distance[neighbor] = min(distance[neighbor], 
62                                        distance[currentNode] + adjacencyMatrix[currentNode][neighbor]);
63            }
64        }
65      
66        // Return result: -1 if unreachable, otherwise the shortest distance
67        return distance[node2] >= INF ? -1 : distance[node2];
68    }
69  
70private:
71    vector<vector<int>> adjacencyMatrix;  // Adjacency matrix representation of graph
72    int nodeCount;                        // Number of nodes in the graph
73    const int INF = 1 << 29;              // Large value representing infinity (no connection)
74};
75
76/**
77 * Your Graph object will be instantiated and called as such:
78 * Graph* obj = new Graph(n, edges);
79 * obj->addEdge(edge);
80 * int param_2 = obj->shortestPath(node1,node2);
81 */
82
1// Global adjacency matrix to store the graph
2let adjacencyMatrix: number[][] = [];
3
4/**
5 * Initializes the graph with n nodes and given edges
6 * @param n - Number of nodes in the graph
7 * @param edges - Array of edges where each edge is [from, to, cost]
8 */
9function initializeGraph(n: number, edges: number[][]): void {
10    // Create n x n matrix initialized with Infinity (no direct connection)
11    adjacencyMatrix = Array.from({ length: n }, () => Array(n).fill(Infinity));
12  
13    // Add all initial edges to the graph
14    for (const [fromNode, toNode, cost] of edges) {
15        adjacencyMatrix[fromNode][toNode] = cost;
16    }
17}
18
19/**
20 * Adds a new edge to the graph
21 * @param edge - Array containing [from, to, cost]
22 */
23function addEdge(edge: number[]): void {
24    const [fromNode, toNode, cost] = edge;
25    adjacencyMatrix[fromNode][toNode] = cost;
26}
27
28/**
29 * Finds the shortest path between two nodes using Dijkstra's algorithm
30 * @param node1 - Starting node
31 * @param node2 - Destination node
32 * @returns The shortest distance from node1 to node2, or -1 if no path exists
33 */
34function shortestPath(node1: number, node2: number): number {
35    const nodeCount = adjacencyMatrix.length;
36  
37    // Initialize distances array with Infinity for all nodes
38    const distances: number[] = Array(nodeCount).fill(Infinity);
39    distances[node1] = 0; // Distance to starting node is 0
40  
41    // Track visited nodes
42    const visited: boolean[] = Array(nodeCount).fill(false);
43  
44    // Process all nodes
45    for (let iteration = 0; iteration < nodeCount; ++iteration) {
46        // Find the unvisited node with minimum distance
47        let currentNode = -1;
48        for (let node = 0; node < nodeCount; ++node) {
49            if (!visited[node] && (currentNode === -1 || distances[node] < distances[currentNode])) {
50                currentNode = node;
51            }
52        }
53      
54        // Mark current node as visited
55        visited[currentNode] = true;
56      
57        // Update distances to all neighbors of current node
58        for (let neighbor = 0; neighbor < nodeCount; ++neighbor) {
59            distances[neighbor] = Math.min(
60                distances[neighbor], 
61                distances[currentNode] + adjacencyMatrix[currentNode][neighbor]
62            );
63        }
64    }
65  
66    // Return the shortest distance to destination node, or -1 if unreachable
67    return distances[node2] >= Infinity ? -1 : distances[node2];
68}
69

Time and Space Complexity

Time Complexity:

  • __init__: O(n^2 + m) where n is the number of nodes and m is the number of initial edges. Creating the n × n matrix takes O(n^2) time, and filling in m edges takes O(m) time.
  • addEdge: O(1) - simply updates one cell in the matrix.
  • shortestPath: O(n^2) - implements Dijkstra's algorithm with a naive approach. The outer loop runs n times, and for each iteration, finding the minimum unvisited node takes O(n) time, and updating distances for all neighbors also takes O(n) time.

Overall, if there are q calls to shortestPath, the total time complexity for all shortest path queries is O(n^2 × q).

Space Complexity:

  • The adjacency matrix self.g requires O(n^2) space to store all possible edges.
  • The shortestPath method uses O(n) additional space for the dist and vis arrays.
  • Overall space complexity is O(n^2) as the matrix dominates the space usage.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Not Handling Self-Loops Properly

The current implementation doesn't initialize the diagonal of the adjacency matrix (self-loops) to 0. This means adjacency_matrix[i][i] = inf for all nodes, which can cause issues when the shortest path from a node to itself is queried.

Problem Example:

g = Graph(3, [[0,1,5], [1,2,3]])
print(g.shortestPath(0, 0))  # Should return 0, but might not work correctly

Solution: Initialize diagonal elements to 0 in the constructor:

def __init__(self, n: int, edges: List[List[int]]):
    self.n = n
    self.adjacency_matrix = [[inf] * n for _ in range(n)]
  
    # Set distance from each node to itself as 0
    for i in range(n):
        self.adjacency_matrix[i][i] = 0
  
    for from_node, to_node, cost in edges:
        self.adjacency_matrix[from_node][to_node] = cost

2. Inefficient Node Selection in Dijkstra's Algorithm

The current implementation uses O(n) linear search to find the minimum unvisited node in each iteration, resulting in O(n²) time complexity for the inner loop alone. For graphs with many shortest path queries, this becomes a bottleneck.

Solution: Use a min-heap (priority queue) to optimize node selection:

import heapq

def shortestPath(self, node1: int, node2: int) -> int:
    distances = [inf] * self.n
    distances[node1] = 0
  
    # Priority queue: (distance, node)
    pq = [(0, node1)]
  
    while pq:
        current_dist, current_node = heapq.heappop(pq)
      
        # Skip if we've already found a better path
        if current_dist > distances[current_node]:
            continue
          
        # Early termination if we reached the destination
        if current_node == node2:
            return current_dist
      
        # Update neighbors
        for neighbor in range(self.n):
            if self.adjacency_matrix[current_node][neighbor] != inf:
                new_dist = current_dist + self.adjacency_matrix[current_node][neighbor]
                if new_dist < distances[neighbor]:
                    distances[neighbor] = new_dist
                    heapq.heappush(pq, (new_dist, neighbor))
  
    return -1 if distances[node2] == inf else distances[node2]

3. Memory Inefficiency for Sparse Graphs

Using an n×n adjacency matrix consumes O(n²) space even for sparse graphs with few edges. This becomes problematic for large graphs with thousands of nodes but relatively few edges.

Solution: Use an adjacency list representation for sparse graphs:

def __init__(self, n: int, edges: List[List[int]]):
    self.n = n
    # Use adjacency list instead of matrix
    self.graph = {i: {} for i in range(n)}
  
    for from_node, to_node, cost in edges:
        self.graph[from_node][to_node] = cost

def addEdge(self, edge: List[int]) -> None:
    from_node, to_node, cost = edge
    self.graph[from_node][to_node] = cost

4. Unnecessary Computation When Path Doesn't Exist

The current implementation processes all n nodes even when it's clear early on that no path exists to the destination (when all remaining unvisited nodes have infinite distance).

Solution: Add early termination when the selected node has infinite distance:

for _ in range(self.n):
    current_node = -1
    for node in range(self.n):
        if not visited[node] and (current_node == -1 or distances[current_node] > distances[node]):
            current_node = node
  
    # Early termination if no reachable nodes remain
    if distances[current_node] == inf:
        break
  
    visited[current_node] = True
  
    # Rest of the algorithm...
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Consider the classic dynamic programming of fibonacci numbers, what is the recurrence relation?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More