Design Graph With Shortest Path Calculator
HardPrompt
There is a directed weighted graph that consists of n nodes numbered from 0 to n - 1. The edges of the graph are initially represented by the given array edges where edges[i] = [fromi, toi, edgeCosti] meaning that there is an edge from fromi to toi with the cost edgeCosti.
Implement the Graph class:
Graph(int n, int[][] edges)initializes the object withnnodes and the given edges.addEdge(int[] edge)adds an edge to the list of edges whereedge = [from, to, edgeCost]. It is guaranteed that there is no edge between the two nodes before adding this one.int shortestPath(int node1, int node2)returns the minimum cost of a path fromnode1tonode2. If no path exists, return-1. The cost of a path is the sum of the costs of the edges in the path.
Example 1:
Input
["Graph", "shortestPath", "shortestPath", "addEdge", "shortestPath"]
[[4, [[0, 2, 5], [0, 1, 2], [1, 2, 1], [3, 0, 3]]], [3, 2], [0, 3], [[1, 3, 4]], [0, 3]]
Output
[null, 6, -1, null, 6]
Explanation
Graph g = new Graph(4, [[0, 2, 5], [0, 1, 2], [1, 2, 1], [3, 0, 3]]);
g.shortestPath(3, 2); // return 6. The shortest path from 3 to 2 in the first diagram above is 3 -> 0 -> 1 -> 2 with a total cost of 3 + 2 + 1 = 6.
g.shortestPath(0, 3); // return -1. There is no path from 0 to 3.
g.addEdge([1, 3, 4]); // We add an edge from node 1 to node 3, and we get the second diagram above.
g.shortestPath(0, 3); // return 6. The shortest path from 0 to 3 now is 0 -> 1 -> 3 with a total cost of 2 + 4 = 6.
Constraints:
1 <= n <= 1000 <= edges.length <= n * (n - 1)edges[i].length == edge.length == 30 <= fromi, toi, from, to, node1, node2 <= n - 11 <= edgeCosti, edgeCost <= 106- There are no repeated edges and no self-loops in the graph at any point.
- At most
100calls will be made foraddEdge. - At most
100calls will be made forshortestPath.
Approaches
2 approaches with complexity analysis and trade-offs.
This approach calculates the shortest path only when the shortestPath method is called. It uses an adjacency list to represent the graph, which is efficient for storing graph structures. Adding a new edge is a very fast operation. The main computation happens inside the shortestPath method, where Dijkstra's algorithm is run from the source node to find the shortest path to the destination node.
Algorithm
Graph(n, edges):- Create an adjacency list
adjof sizen. - For each edge
(u, v, w)in the inputedges, add a pair(v, w)to the listadj[u].
- Create an adjacency list
addEdge(edge):- Given a new edge
(u, v, w), add the pair(v, w)to the listadj[u].
- Given a new edge
shortestPath(node1, node2):- Initialize a
distancesarray of sizenwith infinity, and setdistances[node1] = 0. - Create a priority queue
pqto store pairs of(cost, node), ordered by cost. - Add
(0, node1)topq. - While
pqis not empty: a. Extract the nodeuwith the smallest costdfrompq. b. Ifuis the destinationnode2, returnd. c. Ifdis greater than the already known distance tou, skip. d. For each neighborvofuwith edge weightw: i. If a shorter path is found (distances[u] + w < distances[v]): - Updatedistances[v] = distances[u] + w. - Add(distances[v], v)topq. - If the loop completes and
node2was not reached, return -1.
- Initialize a
Walkthrough
The graph is stored using an adjacency list, where for each node, we store a list of its outgoing edges and their costs. A List<List<int[]>> is a suitable data structure, where adj.get(u) contains pairs of [v, cost] for all edges from u to v.
Initialization (Graph constructor):
- Create an empty adjacency list for
nnodes. - Iterate through the initial
edgesarray and populate the adjacency list. For each edge[from, to, cost], add[to, cost]to the list forfrom.
Adding an Edge (addEdge):
- This is a simple and fast operation. Given an edge
[from, to, cost], just add[to, cost]to the adjacency list of thefromnode.
Finding Shortest Path (shortestPath):
- This is where the main work is done. We run Dijkstra's algorithm starting from
node1. - We need a priority queue to efficiently retrieve the node with the smallest distance to visit next. The priority queue will store pairs of
[cost, node]. - We also need a
distancesarray to keep track of the minimum cost found so far fromnode1to every other node. Initialize all distances to infinity, except fordistances[node1], which is 0. - Start by adding
[0, node1]to the priority queue. - While the priority queue is not empty, extract the node
uwith the minimum cost. If this node is the destination, we have found the shortest path. - For each neighbor
vofu, if a shorter path tovis found (i.e.,distances[u] + cost(u,v) < distances[v]), updatedistances[v]and add[distances[v], v]to the priority queue. - After the algorithm terminates, if the destination was reached, its distance is returned. Otherwise, no path exists, and we return -1.
import java.util.*; class Graph { private int n; private List<List<int[]>> adj; public Graph(int n, int[][] edges) { this.n = n; adj = new ArrayList<>(n); for (int i = 0; i < n; i++) { adj.add(new ArrayList<>()); } for (int[] edge : edges) { adj.get(edge[0]).add(new int[]{edge[1], edge[2]}); } } public void addEdge(int[] edge) { adj.get(edge[0]).add(new int[]{edge[1], edge[2]}); } public int shortestPath(int node1, int node2) { int[] dist = new int[n]; Arrays.fill(dist, Integer.MAX_VALUE); dist[node1] = 0; // PriorityQueue stores {distance, node} PriorityQueue<int[]> pq = new PriorityQueue<>(Comparator.comparingInt(a -> a[0])); pq.offer(new int[]{0, node1}); while (!pq.isEmpty()) { int[] current = pq.poll(); int d = current[0]; int u = current[1]; if (u == node2) { return d; } if (d > dist[u]) { continue; } for (int[] edge : adj.get(u)) { int v = edge[0]; int weight = edge[1]; if (dist[u] + weight < dist[v]) { dist[v] = dist[u] + weight; pq.offer(new int[]{dist[v], v}); } } } return -1; }}Complexity
Time
- `Graph()`: O(E), where E is the initial number of edges. - `addEdge()`: O(1). - `shortestPath()`: O(E' log n), where E' is the current number of edges. In the worst case, E' is O(n^2), making the complexity O(n^2 log n).
Space
O(n + E'), where `n` is the number of nodes and `E'` is the current number of edges. This space is used for the adjacency list.
Trade-offs
Pros
addEdgeis very fast, taking constant time O(1).Space-efficient for sparse graphs, as it only stores existing edges.
Conceptually straightforward if Dijkstra's algorithm is familiar.
Cons
shortestPathcan be slow if called frequently, as it re-computes paths from scratch every time.For dense graphs, the performance degrades to O(n^2 log n) per query, which can be slower than other approaches for the given constraints.
This approach pre-computes the shortest paths between all pairs of nodes using the Floyd-Warshall algorithm and stores them in a 2D matrix. This makes shortestPath queries instantaneous (O(1)). When a new edge is added, instead of a full O(n^3) re-computation, an optimized O(n^2) update step is performed. For the given problem constraints, this trade-off makes it more efficient overall than re-running Dijkstra's algorithm for each query.
Algorithm
Graph(n, edges):- Create an
n x nmatrixdist, initialized with a large value for infinity, anddist[i][i] = 0. - For each initial edge
(u, v, w), setdist[u][v] = w. - Run the Floyd-Warshall algorithm: for
kfrom 0 ton-1, then forifrom 0 ton-1, and forjfrom 0 ton-1, updatedist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]).
- Create an
addEdge(edge):- Let the new edge be
(u, v, w). - For each pair of nodes
(i, j), check if the path through the new edge is shorter:dist[i][j] = min(dist[i][j], dist[i][u] + w + dist[v][j]).
- Let the new edge be
shortestPath(node1, node2):- Look up
dist[node1][node2]. - If the value is infinity, return -1. Otherwise, return the value.
- Look up
Walkthrough
We maintain an n x n matrix, dist, where dist[i][j] stores the cost of the shortest path from node i to node j.
Initialization (Graph constructor):
- Initialize the
distmatrix:dist[i][i] = 0anddist[i][j] = infinityfori != j. Using alongtype for distances prevents overflow. - Populate the matrix with the initial edges: for each edge
(u, v, w), setdist[u][v] = w. - Run the full Floyd-Warshall algorithm with three nested loops (
k,i,j) to compute all-pairs shortest paths. The update rule isdist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]).
Adding an Edge (addEdge):
- When a new edge
(u, v, w)is added, we don't need to re-run the entire O(n^3) algorithm. - The new edge can potentially shorten the path between any pair of nodes
(i, j). A new potential path isi -> ... -> u -> v -> ... -> j. - The cost of this new path is
dist[i][u] + w + dist[v][j]. - We iterate through all pairs
(i, j)and updatedist[i][j]if this new path is shorter. This update step takes O(n^2) time.
Finding Shortest Path (shortestPath):
- This is a simple O(1) lookup. Return
dist[node1][node2]. - If
dist[node1][node2]is infinity, no path exists, so return -1.
import java.util.Arrays; class Graph { private int n; private long[][] dist; private static final long INF = (long)1e12; // A large value for infinity public Graph(int n, int[][] edges) { this.n = n; dist = new long[n][n]; for (int i = 0; i < n; i++) { Arrays.fill(dist[i], INF); dist[i][i] = 0; } for (int[] edge : edges) { dist[edge[0]][edge[1]] = edge[2]; } for (int k = 0; k < n; k++) { for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { dist[i][j] = Math.min(dist[i][j], dist[i][k] + dist[k][j]); } } } } public void addEdge(int[] edge) { int u = edge[0]; int v = edge[1]; long w = edge[2]; // Check if this new edge can shorten any existing path for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { if (dist[i][u] < INF && dist[v][j] < INF) { dist[i][j] = Math.min(dist[i][j], dist[i][u] + w + dist[v][j]); } } } } public int shortestPath(int node1, int node2) { if (dist[node1][node2] >= INF) { return -1; } return (int) dist[node1][node2]; }}Complexity
Time
- `Graph()`: O(n^3) for the initial Floyd-Warshall computation. - `addEdge()`: O(n^2) for the optimized update. - `shortestPath()`: O(1).
Space
O(n^2) to store the all-pairs shortest path matrix.
Trade-offs
Pros
shortestPathqueries are extremely fast, taking O(1) time.addEdgeis faster (O(n^2)) than a full re-computation (O(n^3)).For the given constraints (n <= 100, frequent
addEdgeandshortestPathcalls), this approach is more performant overall.
Cons
High initial setup cost of O(n^3) in the constructor.
High space complexity of O(n^2), which can be an issue for very large
n(though acceptable for n <= 100).
Solutions
Solution
public class Graph { private int n ; private int [][] g ; private readonly int inf = 1 << 29 ; public Graph ( int n , int [][] edges ) { this . n = n ; g = new int [ n ][]; for ( int i = 0 ; i < n ; i ++) { g [ i ] = new int [ n ]; for ( int j = 0 ; j < n ; j ++) { g [ i ][ j ] = inf ; } } foreach ( int [] e in edges ) { g [ e [ 0 ]][ e [ 1 ]] = e [ 2 ]; } } public void AddEdge ( int [] edge ) { g [ edge [ 0 ]][ edge [ 1 ]] = edge [ 2 ]; } public int ShortestPath ( int node1 , int node2 ) { int [] dist = new int [ n ]; bool [] vis = new bool [ n ]; Array . Fill ( dist , inf ); dist [ node1 ] = 0 ; for ( int i = 0 ; i < n ; i ++) { int t = - 1 ; for ( int j = 0 ; j < n ; j ++) { if (! vis [ j ] && ( t == - 1 || dist [ t ] > dist [ j ])) t = j ; } vis [ t ] = true ; for ( int j = 0 ; j < n ; j ++) { dist [ j ] = Math . Min ( dist [ j ], dist [ t ] + g [ t ][ j ]); } } return dist [ node2 ] >= inf ? - 1 : dist [ node2 ]; } } /** * Your Graph object will be instantiated and called as such: * Graph obj = new Graph(n, edges); * obj.AddEdge(edge); * int param_2 = obj.ShortestPath(node1,node2); */class Graph { private int n ; private int [][] g ; private final int inf = 1 << 29 ; public Graph ( int n , int [][] edges ) { this . n = n ; g = new int [ n ][ n ]; for ( var f : g ) { Arrays . fill ( f , inf ); } for ( int [] e : edges ) { int f = e [ 0 ], t = e [ 1 ], c = e [ 2 ]; g [ f ][ t ] = c ; } } public void addEdge ( int [] edge ) { int f = edge [ 0 ], t = edge [ 1 ], c = edge [ 2 ]; g [ f ][ t ] = c ; } public int shortestPath ( int node1 , int node2 ) { int [] dist = new int [ n ]; boolean [] vis = new boolean [ n ]; Arrays . fill ( dist , inf ); dist [ node1 ] = 0 ; for ( int i = 0 ; i < n ; ++ i ) { int t = - 1 ; for ( int j = 0 ; j < n ; ++ j ) { if (! vis [ j ] && ( t == - 1 || dist [ t ] > dist [ j ])) { t = j ; } } vis [ t ] = true ; for ( int j = 0 ; j < n ; ++ j ) { dist [ j ] = Math . min ( dist [ j ], dist [ t ] + g [ t ][ j ]); } } return dist [ node2 ] >= inf ? - 1 : dist [ node2 ]; } } /** * Your Graph object will be instantiated and called as such: * Graph obj = new Graph(n, edges); * obj.addEdge(edge); * int param_2 = obj.shortestPath(node1,node2); */class Graph { public: Graph ( int n , vector < vector < int >>& edges ) { this -> n = n ; g = vector < vector < int >> ( n , vector < int > ( n , inf )); for ( auto & e : edges ) { int f = e [ 0 ], t = e [ 1 ], c = e [ 2 ]; g [ f ][ t ] = c ; } } void addEdge ( vector < int > edge ) { int f = edge [ 0 ], t = edge [ 1 ], c = edge [ 2 ]; g [ f ][ t ] = c ; } int shortestPath ( int node1 , int node2 ) { vector < bool > vis ( n ); vector < int > dist ( n , inf ); dist [ node1 ] = 0 ; for ( int i = 0 ; i < n ; ++ i ) { int t = - 1 ; for ( int j = 0 ; j < n ; ++ j ) { if ( ! vis [ j ] && ( t == - 1 || dist [ t ] > dist [ j ])) { t = j ; } } vis [ t ] = true ; for ( int j = 0 ; j < n ; ++ j ) { dist [ j ] = min ( dist [ j ], dist [ t ] + g [ t ][ j ]); } } return dist [ node2 ] >= inf ? - 1 : dist [ node2 ]; } private: vector < vector < int >> g ; int n ; const int inf = 1 << 29 ; }; /** * Your Graph object will be instantiated and called as such: * Graph* obj = new Graph(n, edges); * obj->addEdge(edge); * int param_2 = obj->shortestPath(node1,node2); */class Graph : def __init__ ( self , n : int , edges : List [ List [ int ]]): self . n = n self . g = [[ inf ] * n for _ in range ( n )] for f , t , c in edges : self . g [ f ][ t ] = c def addEdge ( self , edge : List [ int ]) -> None : f , t , c = edge self . g [ f ][ t ] = c def shortestPath ( self , node1 : int , node2 : int ) -> int : dist = [ inf ] * self . n dist [ node1 ] = 0 vis = [ False ] * self . n for _ in range ( self . n ): t = - 1 for j in range ( self . n ): if not vis [ j ] and ( t == - 1 or dist [ t ] > dist [ j ]): t = j vis [ t ] = True for j in range ( self . n ): dist [ j ] = min ( dist [ j ], dist [ t ] + self . g [ t ][ j ]) return - 1 if dist [ node2 ] == inf else dist [ node2 ] # Your Graph object will be instantiated and called as such: # obj = Graph(n, edges) # obj.addEdge(edge) # param_2 = obj.shortestPath(node1,node2)Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.