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:
-
Constructor
[Graph](/problems/graph_intro)(int n, int[][] edges)
: Initialize the graph withn
nodes and a list of edges. Each edge is represented as[from, to, edgeCost]
, meaning there's a directed edge from nodefrom
to nodeto
with weightedgeCost
. -
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. -
Find Shortest Path
shortestPath(int node1, int node2)
: Calculate and return the minimum cost to travel fromnode1
tonode2
. The cost of a path is the sum of all edge weights along that path. If no path exists fromnode1
tonode2
, 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.
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 nodei
vis[i]
: Whether we've confirmed the shortest path to nodei
We start by setting dist[node1] = 0
(distance to source is 0) and all other distances to infinity. Then, we repeatedly:
- Pick the unvisited node
t
with the smallest distance - Mark
t
as visited (we've found its shortest path) - 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 matrixg
where all values are initialized toinf
(infinity) - For each edge
[f, t, c]
in the input edges list, setg[f][t] = c
to represent a directed edge from nodef
to nodet
with costc
- 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:
-
Initialize distance array: Create
dist
array of sizen
with all values set toinf
, exceptdist[node1] = 0
(source node has distance 0) -
Initialize visited array: Create
vis
array of sizen
with all values set toFalse
-
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 nodet
-
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 nodej
, updatedist[j]
-
-
Return result: After processing all nodes, check if
dist[node2] == inf
. If so, return-1
(no path exists). Otherwise, returndist[node2]
(the shortest path distance)
Time Complexity:
__init__
: O(n² + E) where E is the number of initial edgesaddEdge
: 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 EvaluatorExample 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)
wheren
is the number of nodes andm
is the number of initial edges. Creating then × n
matrix takesO(n^2)
time, and filling inm
edges takesO(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 runsn
times, and for each iteration, finding the minimum unvisited node takesO(n)
time, and updating distances for all neighbors also takesO(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
requiresO(n^2)
space to store all possible edges. - The
shortestPath
method usesO(n)
additional space for thedist
andvis
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...
Consider the classic dynamic programming of fibonacci numbers, what is the recurrence relation?
Recommended Readings
https assets algo monster cover_photos graph svg Graph Fundamentals Tree with 0 cycle At this point you should be pretty familiar with trees A tree is a special kind of graph a connected acyclic cycle less graph A graph may contain cycle s and nodes could be disconnected A tree
Shortest Path Between A and B Prereq BFS on Graph problems graph_bfs Given an unweighted connected graph return the length of the shortest path between two nodes A and B in terms of the number of edges Assume there always exists a path between nodes A and B Input graph
https assets algo monster cover_photos heap svg Priority Queue and Heap What is the relationship between priority queue and heap Priority Queue is an Abstract Data Type and Heap is the concrete data structure we use to implement a priority queue Priority Queue A priority queue is a data structure
Want a Structured Path to Master System Design Too? Don’t Miss This!