2642. Design Graph With Shortest Path Calculator


Problem Description

In this problem, we are tasked with implementing a class, [Graph](/problems/graph_intro), that represents a directed weighted graph. A graph is composed of nodes (or vertices) connected by edges, each with an associated cost. The Graph class is supposed to support the following functionalities:

  • Initialization with a specific number of nodes and a list of edges, where each edge is a triplet [from, to, edgeCost] representing an edge from node from to node to with a cost edgeCost.

  • Adding a new edge to the graph which did not previously exist between two nodes.

  • Finding the shortest path between any two nodes, which is the path where the sum of the costs of its constituent edges is the lowest possible. If no such path exists, the method should return -1.

The challenge in this problem is efficiently implementing these functionalities, particularly the path-finding operation, which is central to many applications in network analysis, logistics, and more.

Intuition

The intuition behind the solution lies in implementing effective ways to manage and query the graph upon which we can apply a shortest path algorithm. To represent the graph internally, we use a two-dimensional matrix (self.g). This allows us to quickly access the cost of any edge given a starting and ending node, defaulting to 'infinity' (inf) where edges do not exist.

For the shortest path algorithm, we can use a variant of Dijkstra's algorithm, which is well-known for finding the shortest paths between nodes in a graph with non-negative edge weights. Here's the high-level approach:

  1. Initialize a dist array, representing the shortest discovered distances from node1 to every other node, with infinite values except for the starting node (node1), which has a distance of zero.

  2. Create a vis array to keep track of nodes whose shortest distance is finalized and will no longer change.

  3. Repeat the following steps n times (where n is the number of nodes in the graph):

    a. Select the unvisited node with the current smallest known distance (t).

    b. Mark this node as visited.

    c. Update the dist values of adjacent nodes using the edge costs from the selected node if it results in a shorter path than what is currently known.

  4. Once completed, the dist array contains the shortest known distances from node1 to all other nodes. Specifically, dist[node2] will give us the minimum cost of the path from node1 to node2. If this value is inf, it means no path exists, and we return -1.

With this approach, addEdge simply updates the edge matrix self.g with the new edge cost, whereas shortestPath performs Dijkstra's algorithm as described to return the shortest path cost between two given nodes.

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

Solution Approach

The implementation of the [Graph](/problems/graph_intro) class consists of several components, each handling a distinct aspect of the graph's functionality. Here's a detailed breakdown:

  1. Initialization (__init__ method):

    • We initialize a 2D matrix self.g with size n * n, where n is the number of nodes. This matrix will hold the edge costs, initialized to 'infinity' (inf) to represent no direct edge between nodes. This value is symbolic for the absence of a path and ensures it doesn't interfere with the calculation of minimum costs.
    • For each edge in the input list edges, we populate self.g with the corresponding edge cost at the position [from_i][to_i], where from_i and to_i are the starting and ending nodes of the edge i with cost edgeCost_i.
  2. Adding an Edge (addEdge method):

    • It's a straightforward update of the edge cost matrix self.g for the new edge specified by the edge parameter.
    • The new edge's starting node, ending node, and cost are extracted from the input edge list and used to update self.g.
  3. Finding the Shortest Path (shortestPath method):

    • Initialize a list dist with values 'infinity' (inf) for tracking the shortest distance from node1 to all other nodes. We set dist[node1] to 0 since the distance from node1 to itself is zero.
    • Create a list vis that will track whether we have finalized the shortest distance to a node. Initially, all values are set to False.
    • We use a for-loop to iterate n times, where n is the number of nodes:
      • Within this loop, we select the node with the smallest dist value that hasn't been visited yet (t). This greedy choice leads us to pick the nearest unvisited node.
      • We mark this node t as visited by setting vis[t] to True.
      • We then relax the edges by iterating over all nodes (j) and checking if the path through t offers a shorter path. If it does, we update dist[j] with the new minimum cost.
    • Finally, we check if dist[node2] is still 'infinity' (inf) to determine if node1 and node2 are disconnected; if so, we return -1. Otherwise, we return dist[node2], which is the minimum cost of the path found from node1 to node2.

Each step of the shortestPath method follows the standard Dijkstra's algorithm pattern with a slight modification to adapt it to a dense graph representation (2D matrix). This approach ensures that we can find the shortest path efficiently, even in a graph where the edges are represented with a matrix instead of an adjacency list or other sparse representation.

Ready to land your dream job?

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

Start Evaluator

Example Walkthrough

Let's use a small example to illustrate the solution approach. Suppose we have a graph with 4 nodes (numbered 0 through 3) and the following directed edges:

  • An edge from node 0 to node 1 with a cost of 3
  • An edge from node 1 to node 2 with a cost of 4
  • An edge from node 2 to node 3 with a cost of 2
  • An edge from node 0 to node 3 with a cost of 10

We want to find the shortest path from node 0 to node 3.

Step 1: Initialization

We instantiate a Graph object with 4 nodes and an initial list of edges: [(0, 1, 3), (1, 2, 4), (2, 3, 2), (0, 3, 10)].

Step 2: Representation

After initialization, our graph's internal matrix self.g will look like this, where inf denotes no direct edge:

   0  1    2    3
0  0  3  inf   10
1 inf  0    4  inf
2 inf inf   0    2
3 inf inf  inf    0

Step 3: Add a New Edge

Let's say we add a new edge from node 1 to node 3 with a cost of 5 using the addEdge method:

   0  1    2    3
0  0  3  inf   10
1 inf  0    4    5
2 inf inf   0    2
3 inf inf  inf    0

Step 4: Finding the Shortest Path

Now, to find the shortest path from node 0 to node 3, we follow Dijkstra's algorithm:

  • Initialize dist with inf and set dist[0] to 0.
  • The vis array will keep track of visited nodes, starting all as False.

For each iteration:

  1. Pick the smallest dist value from unvisited nodes. First, it's 0 itself, with a distance of 0.

  2. Visit node 0, and update distances to its adjacent nodes. dist[1] becomes 3, and dist[3] becomes 10.

  3. Next, pick node 1 since it has the smallest dist of 3. Visit and update its neighbors. Since 1 -> 3 is 5, the total cost 0 -> 1 -> 3 would be 3 + 5 = 8, which is less than the existing 10. So, dist[3] is updated to 8.

  4. Repeat the process for nodes 2 and 3, but since 0 -> 1 -> 3 is already the smallest path to 3, there will be no more updates.

In the end, dist[3] equals 8, which is the shortest path cost from 0 to 3.

This would conclude our example, demonstrating how we would use the provided Graph class to add edges and find the shortest path between two nodes.

Solution Implementation

1from typing import List
2
3# Define infinity as a variable for ease of use and readability
4inf = float('inf')
5
6class Graph:
7    # Initialize the graph with 'n' nodes and the specified 'edges'.
8    def __init__(self, n: int, edges: List[List[int]]):
9        self.n = n  # Number of nodes
10        self.adj_matrix = [[inf] * n for _ in range(n)]  # Adjacency matrix to store edge costs
11      
12        # Fill the adjacency matrix with the cost of each edge
13        for source, target, cost in edges:
14            self.adj_matrix[source][target] = cost
15
16    # Add an edge to the graph with a source, target, and cost.
17    def add_edge(self, edge: List[int]) -> None:
18        source, target, cost = edge
19        self.adj_matrix[source][target] = cost
20
21    # Compute the shortest path from 'node1' to 'node2' using Dijkstra's algorithm.
22    def shortest_path(self, node1: int, node2: int) -> int:
23        dist = [inf] * self.n  # Distances array
24        dist[node1] = 0        # Distance to starting node is always 0
25        visited = [False] * self.n  # Visited array to mark nodes
26      
27        # Perform Dijkstra's algorithm to find the shortest paths
28        for _ in range(self.n):
29            closest_node = -1
30            for j in range(self.n):
31                if not visited[j] and (closest_node == -1 or dist[closest_node] > dist[j]):
32                    closest_node = j
33            visited[closest_node] = True
34          
35            # Update distances to adjacent nodes
36            for j in range(self.n):
37                dist[j] = min(dist[j], dist[closest_node] + self.adj_matrix[closest_node][j])
38      
39        # Return the shortest path cost to 'node2', or -1 if 'node2' is not reachable
40        return -1 if dist[node2] == inf else dist[node2]
41
42
43# Example of how the Graph class could be utilized
44# obj = Graph(n, edges)
45# obj.add_edge(edge)
46# param_2 = obj.shortest_path(node1, node2)
47
1import java.util.Arrays;
2
3public class Graph {
4    private int numberOfNodes;
5    private int[][] adjacencyMatrix;
6    private static final int INFINITY = 1 << 29; // A representation of infinity (large value)
7
8    // Constructor to initialize the graph with a number of nodes and edges.
9    public Graph(int numberOfNodes, int[][] edges) {
10        this.numberOfNodes = numberOfNodes;
11        adjacencyMatrix = new int[numberOfNodes][numberOfNodes];
12      
13        // Initialize all distances in the adjacency matrix to infinity.
14        for (int[] row : adjacencyMatrix) {
15            Arrays.fill(row, INFINITY);
16        }
17      
18        // Fill the adjacency matrix with the given edges and their costs.
19        for (int[] edge : edges) {
20            int from = edge[0], to = edge[1], cost = edge[2];
21            adjacencyMatrix[from][to] = cost;
22        }
23    }
24
25    // Method to add an edge with a cost to the graph.
26    public void addEdge(int[] edge) {
27        int from = edge[0], to = edge[1], cost = edge[2];
28        adjacencyMatrix[from][to] = cost;
29    }
30
31    // Method to find the shortest path from one node to another using Dijkstra's algorithm.
32    public int shortestPath(int sourceNode, int targetNode) {
33        int[] distances = new int[numberOfNodes];
34        boolean[] visited = new boolean[numberOfNodes];
35      
36        // Initialize all distances to infinity, except the distance to the source node itself.
37        Arrays.fill(distances, INFINITY);
38        distances[sourceNode] = 0;
39      
40        // Iterate through all nodes to pick the one with the minimum distance, which is not yet visited.
41        for (int i = 0; i < numberOfNodes; ++i) {
42            int closest = -1;
43            for (int j = 0; j < numberOfNodes; ++j) {
44                if (!visited[j] && (closest == -1 || distances[closest] > distances[j])) {
45                    closest = j;
46                }
47            }
48            visited[closest] = true;
49          
50            // Update the distance for each node adjacent to the currently selected node.
51            for (int j = 0; j < numberOfNodes; ++j) {
52                distances[j] = Math.min(distances[j], distances[closest] + adjacencyMatrix[closest][j]);
53            }
54        }
55      
56        // Return the shortest distance to the target node, or -1 if not reachable.
57        return distances[targetNode] >= INFINITY ? -1 : distances[targetNode];
58    }
59
60    //...
61
62    // Note: If the Graph class is part of a larger file or project, keep the ellipsis and add additional methods here.
63}
64
65/**
66 * Usage example (outside the Graph class):
67 * Graph obj = new Graph(n, edges);
68 * obj.addEdge(edge);
69 * int shortestDistance = obj.shortestPath(node1, node2);
70 */
71
1#include <vector>
2#include <algorithm>
3#include <climits> // for INT_MAX
4
5using namespace std;
6
7class Graph {
8public:
9    // Construct a graph with n nodes and initialized edges
10    Graph(int n, const vector<vector<int>>& edges) {
11        this->numNodes = n;
12        // Initialize graph with 'numNodes' and set distances to infinity (INT_MAX represents infinity)
13        adjMatrix = vector<vector<int>>(numNodes, vector<int>(numNodes, INT_MAX));
14        // Set edge distances based on the input edges
15        for (const auto& edge : edges) {
16            int from = edge[0], to = edge[1], cost = edge[2];
17            adjMatrix[from][to] = cost;
18        }
19    }
20
21    // Adds a new edge to the graph
22    void addEdge(const vector<int>& edge) {
23        int from = edge[0], to = edge[1], cost = edge[2];
24        adjMatrix[from][to] = cost;
25    }
26
27    // Computes the shortest path between node1 and node2 using Dijkstra's algorithm
28    int shortestPath(int startNode, int endNode) {
29        vector<bool> visited(numNodes, false);
30        vector<int> distances(numNodes, INT_MAX);
31        distances[startNode] = 0;
32
33        // Iterate 'numNodes' times to find the shortest paths
34        for (int i = 0; i < numNodes; ++i) {
35            int closest = -1;
36            // Find the nearest unvisited node
37            for (int j = 0; j < numNodes; ++j) {
38                if (!visited[j] && (closest == -1 || distances[closest] > distances[j])) {
39                    closest = j;
40                }
41            }
42            visited[closest] = true; // Mark the node as visited
43          
44            // Update the distances of the adjacent nodes
45            for (int j = 0; j < numNodes; ++j) {
46                // Safe addition to avoid integer overflow
47                if(distances[closest] != INT_MAX && adjMatrix[closest][j] != INT_MAX) {
48                    distances[j] = min(distances[j], distances[closest] + adjMatrix[closest][j]);
49                }
50            }
51        }
52        // Return the shortest path to 'endNode', or -1 if it's not reachable
53        return distances[endNode] == INT_MAX ? -1 : distances[endNode];
54    }
55
56private:
57    vector<vector<int>> adjMatrix; // Adjacency matrix to represent the graph
58    int numNodes; // Number of nodes in the graph
59};
60
61/**
62 * Your Graph object will be instantiated and called as such:
63 * Graph* obj = new Graph(numNodes, edges);
64 * obj->addEdge(edge);
65 * int param_2 = obj->shortestPath(startNode, endNode);
66 */
67
1// Define the INF constant to represent an unreachable state in the graph.
2const INF: number = 1 << 29;
3
4// Declare the 'graph' variable to store the adjacency matrix of the graph.
5let graph: number[][] = [];
6
7/**
8 * Initializes the graph with a given number of nodes and an array of edges.
9 *
10 * @param n - The number of nodes in the graph.
11 * @param edges - An array of edges where each edge is represented as [from, to, cost].
12 */
13function initializeGraph(n: number, edges: number[][]): void {
14    graph = Array.from({ length: n }, () => Array(n).fill(INF));
15    // Add each edge to the graph.
16    for (const [from, to, cost] of edges) {
17        graph[from][to] = cost;
18    }
19}
20
21/**
22 * Adds a new edge to the graph.
23 *
24 * @param edge - An array representing the new edge as [from, to, cost].
25 */
26function addEdge(edge: number[]): void {
27    const [from, to, cost] = edge;
28    graph[from][to] = cost;
29}
30
31/**
32 * Finds the shortest path between two nodes using Dijkstra's algorithm.
33 *
34 * @param node1 - The starting node.
35 * @param node2 - The destination node.
36 * @returns The shortest path cost or -1 if no path exists.
37 */
38function shortestPath(node1: number, node2: number): number {
39    const n = graph.length;
40    const dist: number[] = new Array(n).fill(INF);
41    dist[node1] = 0;
42    const visited: boolean[] = new Array(n).fill(false);
43
44    // Find the shortest paths to all nodes.
45    for (let i = 0; i < n; ++i) {
46        let nearest = -1;
47        // Select the unvisited node with the smallest distance.
48        for (let j = 0; j < n; ++j) {
49            if (!visited[j] && (nearest === -1 || dist[j] < dist[nearest])) {
50                nearest = j;
51            }
52        }
53        // Mark as visited.
54        visited[nearest] = true;
55        // Update distances to other nodes.
56        for (let j = 0; j < n; ++j) {
57            dist[j] = Math.min(dist[j], dist[nearest] + graph[nearest][j]);
58        }
59    }
60
61    // Return the distance to the destination node, or -1 if it's not reachable.
62    return dist[node2] >= INF ? -1 : dist[node2];
63}
64
65// Example usage:
66// initializeGraph(n, edges);
67// addEdge(edge);
68// const pathCost = shortestPath(node1, node2);
69

Time and Space Complexity

Time Complexity

  • The constructor __init__ has a time complexity of O(n^2) because it initializes an n x n matrix, where each initialization operation has a constant time complexity, and we perform n * n such operations. Additionally, it iterates through each edge provided to fill in the graph, which adds an O(E) time complexity where E is the number of edges, leading to a total constructor complexity of O(n^2 + E).

  • The addEdge function has a time complexity of O(1) as it performs a constant number of operations regardless of the graph size.

  • For the shortestPath method, there is a nested loop structure. The outer loop runs n times, and for each iteration, the inner loops iterate up to n times to find the unvisited node with the smallest distance and to update the distances. This leads to an overall time complexity of O(n^2) for the shortest path calculation using Dijkstra's algorithm without a priority queue.

Space Complexity

  • The space complexity of the class is O(n^2), mainly due to the adjacency matrix self.g which stores the weights of the edges between nodes.

  • The shortestPath method uses an additional array dist of size n and a boolean array vis also of size n, which account for a space complexity of O(n). Since this does not dominate the O(n^2) space complexity of the class, the overall space complexity remains O(n^2).

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


Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:
Question 1 out of 10

What data structure does Breadth-first search typically uses to store intermediate states?


Recommended Readings

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


Load More