Facebook Pixel

2492. Minimum Score of a Path Between Two Cities

Problem Description

You have n cities numbered from 1 to n. These cities are connected by bidirectional roads, where each road connects two cities and has a specific distance. The road network is given as a 2D array roads, where roads[i] = [ai, bi, distancei] means there's a bidirectional road between city ai and city bi with distance distancei.

The score of a path between two cities is defined as the minimum distance among all roads in that path. Your task is to find the minimum possible score of any path between city 1 and city n.

Key points to understand:

  • A path is a sequence of roads connecting two cities
  • You can use the same road multiple times in a path
  • You can visit cities 1 and n multiple times along the path
  • The cities may not all be connected to each other, but it's guaranteed that there's at least one path between city 1 and city n

Since you can traverse roads multiple times and revisit cities, the problem essentially asks for the smallest edge weight in the connected component containing both city 1 and city n. The solution uses DFS starting from city 1 to explore all reachable cities and roads, tracking the minimum edge weight encountered. The algorithm builds an adjacency list representation of the graph, then performs a depth-first search to visit all nodes in the connected component, updating the minimum edge weight as it explores.

Flowchart Walkthrough

First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:

Is it a graph?

  • Yes: The problem explicitly involves cities (nodes) connected by roads (edges), forming a graph structure where roads[i] = [ai, bi, distancei] represents edges between nodes.

Is it a tree?

  • No: The graph can have cycles (since roads are bidirectional and we can revisit cities), and there's no mention of it being a tree structure. Multiple paths can exist between cities.

Is the problem related to directed acyclic graphs?

  • No: The graph is undirected (bidirectional roads) and can contain cycles.

Is the problem related to shortest paths?

  • No: While it involves distances, we're not finding the shortest path. Instead, we're finding the minimum edge weight in a connected component (since we can traverse edges multiple times).

Does the problem involve connectivity?

  • Yes: The core of the problem is finding all edges in the connected component containing cities 1 and n, since we can use any edge in that component multiple times.

Disjoint Set Union

  • While DSU could work here, the solution uses DFS instead. Both approaches can solve connectivity problems.

Conclusion: The flowchart leads us to connectivity problems, which can be solved using DFS. The DFS approach is appropriate here because we need to explore all reachable nodes and edges from city 1 to find the minimum edge weight in the connected component.

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

Intuition

The key insight comes from understanding what "you can visit cities 1 and n multiple times along the path" really means. Since we can traverse any road multiple times and revisit cities, we're not constrained to a simple path from city 1 to city n.

Think about it this way: if city 1 and city n are connected (which is guaranteed), and we can use roads multiple times, then we can construct a path that goes from city 1 to any reachable city, uses the smallest edge in that connected component, and then continues to city n. We can even go back and forth on that smallest edge as many times as we want!

For example, if we have cities 1-2-3-n connected with edges of weights [10, 5, 8], we could create a path like: 1 β†’ 2 β†’ 3 β†’ 2 β†’ 3 β†’ n. This path includes the edge with weight 5 twice, making the score of this path 5 (the minimum edge weight in the path).

This realization transforms the problem: instead of finding the optimal path from 1 to n, we just need to find the smallest edge weight among all edges that are reachable from city 1. Since city n is guaranteed to be reachable from city 1, any edge in this connected component could potentially be part of our path.

Therefore, the solution approach becomes straightforward:

  1. Start from city 1
  2. Explore all reachable cities using DFS
  3. Track the minimum edge weight encountered during the exploration
  4. Return this minimum value

The DFS ensures we visit every edge in the connected component containing city 1 (and therefore city n), and we simply keep track of the smallest edge weight we find.

Learn more about Depth-First Search, Breadth-First Search, Union Find and Graph patterns.

Solution Approach

Based on our intuition that we need to find the minimum edge weight in the connected component containing city 1, let's implement a DFS solution.

Data Structure Setup:

  • We use an adjacency list g (implemented as a defaultdict(list)) to represent the graph
  • Each city maps to a list of tuples (neighbor, distance) for efficient traversal
  • A visited array vis of size n+1 to track which cities we've explored (indexed from 1 to n)
  • A variable ans initialized to infinity to track the minimum edge weight found

Building the Graph:

g = defaultdict(list)
for a, b, d in roads:
    g[a].append((b, d))
    g[b].append((a, d))

Since roads are bidirectional, we add edges in both directions.

DFS Implementation: The DFS function explores all reachable cities from a starting city i:

def dfs(i):
    nonlocal ans
    for j, d in g[i]:
        ans = min(ans, d)  # Update minimum edge weight
        if not vis[j]:
            vis[j] = True
            dfs(j)

Key aspects of the DFS:

  1. For each city i, we examine all its neighbors j and edge weights d
  2. We update ans with the minimum edge weight encountered (regardless of whether the neighbor has been visited)
  3. We only recursively visit unvisited neighbors to avoid infinite loops
  4. The nonlocal keyword allows us to modify the ans variable from the outer scope

Main Execution:

vis = [False] * (n + 1)
ans = inf
vis[1] = True  # Mark city 1 as visited (implicit in the solution)
dfs(1)         # Start DFS from city 1
return ans

The algorithm explores the entire connected component starting from city 1, examining every edge exactly once (in each direction), and returns the minimum edge weight found. Since city n is guaranteed to be in the same connected component as city 1, we're guaranteed to find the optimal answer.

Time Complexity: O(V + E) where V is the number of cities and E is the number of roads, as we visit each vertex and edge once.

Space Complexity: O(V + E) for the adjacency list and O(V) for the visited array and recursion stack.

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 concrete example to illustrate how the solution works.

Example Graph:

  • Cities: 1, 2, 3, 4
  • Roads: [[1,2,9], [2,3,6], [2,4,5], [1,4,7]]
  • Find the minimum score of a path from city 1 to city 4

Step 1: Build the Adjacency List

g[1] = [(2,9), (4,7)]
g[2] = [(1,9), (3,6), (4,5)]
g[3] = [(2,6)]
g[4] = [(2,5), (1,7)]

Step 2: Initialize Variables

  • vis = [False, False, False, False, False] (index 0 unused, cities 1-4)
  • ans = infinity
  • Mark city 1 as visited: vis[1] = True

Step 3: DFS Traversal from City 1

Starting DFS(1):

  • Examine edge (1,2) with weight 9:

    • Update ans = min(infinity, 9) = 9
    • City 2 not visited, so mark vis[2] = True and call DFS(2)

    DFS(2):

    • Examine edge (2,1) with weight 9:
      • Update ans = min(9, 9) = 9
      • City 1 already visited, skip recursion
    • Examine edge (2,3) with weight 6:
      • Update ans = min(9, 6) = 6
      • City 3 not visited, so mark vis[3] = True and call DFS(3)
      DFS(3):
      • Examine edge (3,2) with weight 6:
        • Update ans = min(6, 6) = 6
        • City 2 already visited, skip recursion
      • Return from DFS(3)
    • Examine edge (2,4) with weight 5:
      • Update ans = min(6, 5) = 5
      • City 4 not visited, so mark vis[4] = True and call DFS(4)
      DFS(4):
      • Examine edge (4,2) with weight 5:
        • Update ans = min(5, 5) = 5
        • City 2 already visited, skip recursion
      • Examine edge (4,1) with weight 7:
        • Update ans = min(5, 7) = 5
        • City 1 already visited, skip recursion
      • Return from DFS(4)
    • Return from DFS(2)
  • Back in DFS(1), examine edge (1,4) with weight 7:

    • Update ans = min(5, 7) = 5
    • City 4 already visited, skip recursion
  • Return from DFS(1)

Step 4: Return Result The minimum edge weight found is 5.

Why This Works: The algorithm found that the smallest edge in the connected component is the edge between cities 2 and 4 with weight 5. Since we can traverse roads multiple times, we can create a path like: 1 β†’ 2 β†’ 4 β†’ 2 β†’ 4, which uses the edge (2,4) twice and has a score of 5 (the minimum edge weight in the path). This is the optimal answer because any path from city 1 to city 4 must have a score at most equal to the smallest edge weight in the connected component.

Solution Implementation

1from collections import defaultdict
2from typing import List
3
4class Solution:
5    def minScore(self, n: int, roads: List[List[int]]) -> int:
6        """
7        Find the minimum score (edge weight) in the connected component containing node 1.
8      
9        Args:
10            n: Number of nodes in the graph (1 to n)
11            roads: List of edges where each edge is [node_a, node_b, distance]
12      
13        Returns:
14            Minimum edge weight in the connected component containing node 1
15        """
16      
17        # Build adjacency list representation of the undirected graph
18        graph = defaultdict(list)
19        for node_a, node_b, distance in roads:
20            # Add edge in both directions since it's undirected
21            graph[node_a].append((node_b, distance))
22            graph[node_b].append((node_a, distance))
23      
24        # Track visited nodes to avoid cycles during DFS
25        visited = [False] * (n + 1)  # Index 0 unused, nodes are 1-indexed
26      
27        # Initialize minimum score to infinity
28        min_score = float('inf')
29      
30        def depth_first_search(current_node):
31            """
32            Perform DFS to explore all nodes in the connected component
33            and find the minimum edge weight.
34          
35            Args:
36                current_node: Current node being explored
37            """
38            nonlocal min_score
39          
40            # Check all edges from current node
41            for neighbor, edge_weight in graph[current_node]:
42                # Update minimum score if we found a smaller edge weight
43                min_score = min(min_score, edge_weight)
44              
45                # Continue DFS if neighbor hasn't been visited
46                if not visited[neighbor]:
47                    visited[neighbor] = True
48                    depth_first_search(neighbor)
49      
50        # Start DFS from node 1
51        visited[1] = True
52        depth_first_search(1)
53      
54        return min_score
55
1class Solution {
2    // Adjacency list representation of the graph
3    private List<int[]>[] adjacencyList;
4    // Visited array to track which nodes have been explored
5    private boolean[] visited;
6    // Minimum edge weight found in the connected component
7    private int minimumEdgeWeight = 1 << 30; // Initialize to a large value (2^30)
8
9    public int minScore(int n, int[][] roads) {
10        // Initialize the graph with n nodes (0-indexed)
11        adjacencyList = new List[n];
12        visited = new boolean[n];
13      
14        // Create an empty list for each node
15        Arrays.setAll(adjacencyList, index -> new ArrayList<>());
16      
17        // Build the undirected graph from the roads
18        for (int[] road : roads) {
19            // Convert to 0-indexed (roads are 1-indexed)
20            int nodeA = road[0] - 1;
21            int nodeB = road[1] - 1;
22            int edgeWeight = road[2];
23          
24            // Add edge in both directions (undirected graph)
25            adjacencyList[nodeA].add(new int[] {nodeB, edgeWeight});
26            adjacencyList[nodeB].add(new int[] {nodeA, edgeWeight});
27        }
28      
29        // Start DFS from node 0 (which is node 1 in the problem)
30        depthFirstSearch(0);
31      
32        return minimumEdgeWeight;
33    }
34
35    private void depthFirstSearch(int currentNode) {
36        // Explore all edges from the current node
37        for (int[] edge : adjacencyList[currentNode]) {
38            int neighborNode = edge[0];
39            int edgeWeight = edge[1];
40          
41            // Update the minimum edge weight found so far
42            minimumEdgeWeight = Math.min(minimumEdgeWeight, edgeWeight);
43          
44            // If the neighbor hasn't been visited, mark it and continue DFS
45            if (!visited[neighborNode]) {
46                visited[neighborNode] = true;
47                depthFirstSearch(neighborNode);
48            }
49        }
50    }
51}
52
1class Solution {
2public:
3    int minScore(int n, vector<vector<int>>& roads) {
4        // Build adjacency list representation of the graph
5        // Each node stores pairs of (neighbor_node, edge_weight)
6        vector<vector<pair<int, int>>> adjacencyList(n);
7      
8        // Track visited nodes during DFS traversal
9        bool visited[n];
10        memset(visited, 0, sizeof(visited));
11      
12        // Construct the undirected graph from roads
13        // Convert 1-indexed nodes to 0-indexed
14        for (auto& road : roads) {
15            int nodeA = road[0] - 1;  // Convert to 0-indexed
16            int nodeB = road[1] - 1;  // Convert to 0-indexed
17            int distance = road[2];
18          
19            // Add edge in both directions (undirected graph)
20            adjacencyList[nodeA].emplace_back(nodeB, distance);
21            adjacencyList[nodeB].emplace_back(nodeA, distance);
22        }
23      
24        // Initialize minimum score to maximum possible value
25        int minEdgeWeight = INT_MAX;
26      
27        // Define DFS function to explore all reachable nodes from node 0
28        // and find the minimum edge weight in the connected component
29        function<void(int)> dfs = [&](int currentNode) {
30            // Explore all neighbors of current node
31            for (auto [neighborNode, edgeWeight] : adjacencyList[currentNode]) {
32                // Update minimum edge weight found so far
33                minEdgeWeight = min(minEdgeWeight, edgeWeight);
34              
35                // Visit unvisited neighbors
36                if (!visited[neighborNode]) {
37                    visited[neighborNode] = true;
38                    dfs(neighborNode);
39                }
40            }
41        };
42      
43        // Start DFS from node 0 (city 1 in 1-indexed)
44        visited[0] = true;
45        dfs(0);
46      
47        // Return the minimum edge weight found in the connected component
48        return minEdgeWeight;
49    }
50};
51
1/**
2 * Finds the minimum score (edge weight) in the connected component containing node 1
3 * @param n - Number of nodes in the graph (nodes are labeled from 1 to n)
4 * @param roads - Array of roads where each road is [nodeA, nodeB, distance]
5 * @returns The minimum edge weight in the connected component containing node 1
6 */
7function minScore(n: number, roads: number[][]): number {
8    // Track visited nodes to avoid revisiting during DFS
9    const visited: boolean[] = new Array(n + 1).fill(false);
10  
11    // Build adjacency list representation of the graph
12    // Index represents node number, value is array of [neighborNode, edgeWeight] pairs
13    const adjacencyList: Array<Array<[number, number]>> = Array.from(
14        { length: n + 1 }, 
15        () => []
16    );
17  
18    // Populate the adjacency list with bidirectional edges
19    for (const [nodeA, nodeB, edgeWeight] of roads) {
20        adjacencyList[nodeA].push([nodeB, edgeWeight]);
21        adjacencyList[nodeB].push([nodeA, edgeWeight]);
22    }
23  
24    // Initialize minimum score to infinity
25    let minimumScore: number = Infinity;
26  
27    /**
28     * Depth-first search to explore all nodes in the connected component
29     * Updates the minimum score as it traverses edges
30     * @param currentNode - The current node being explored
31     */
32    const dfs = (currentNode: number): void => {
33        // Skip if node has already been visited
34        if (visited[currentNode]) {
35            return;
36        }
37      
38        // Mark current node as visited
39        visited[currentNode] = true;
40      
41        // Explore all neighbors of the current node
42        for (const [neighborNode, edgeWeight] of adjacencyList[currentNode]) {
43            // Update minimum score if current edge weight is smaller
44            minimumScore = Math.min(minimumScore, edgeWeight);
45            // Recursively explore the neighbor node
46            dfs(neighborNode);
47        }
48    };
49  
50    // Start DFS from node 1
51    dfs(1);
52  
53    return minimumScore;
54}
55

Time and Space Complexity

Time Complexity: O(n + m), where n is the number of nodes and m is the number of edges (roads).

  • Building the adjacency list takes O(m) time as we iterate through all roads once, and each road creates two entries in the graph (bidirectional edges).
  • The DFS traversal visits each node at most once due to the vis array marking visited nodes, which takes O(n) time.
  • For each visited node, we explore all its adjacent edges. Since each edge is stored twice in the adjacency list (once for each endpoint), we examine at most 2m edges in total, which is O(m).
  • Therefore, the overall time complexity is O(n + m).

Space Complexity: O(n + m)

  • The adjacency list g stores all edges. Since each edge is stored twice (bidirectional), it requires O(m) space.
  • The visited array vis has size n + 1, requiring O(n) space.
  • The recursion stack for DFS can go up to depth n in the worst case (when the graph forms a path), requiring O(n) space.
  • Therefore, the overall space complexity is O(n + m).

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

Common Pitfalls

Pitfall 1: Updating minimum score only for unvisited nodes

A critical mistake is placing the minimum score update inside the condition that checks if a node is unvisited:

Incorrect Implementation:

def depth_first_search(current_node):
    nonlocal min_score
    for neighbor, edge_weight in graph[current_node]:
        if not visited[neighbor]:
            min_score = min(min_score, edge_weight)  # WRONG: Only updates for unvisited
            visited[neighbor] = True
            depth_first_search(neighbor)

Why it's wrong: This approach misses edges that connect to already-visited nodes. Consider a graph where node 1 connects to node 2 with weight 10, and later in the traversal, node 3 connects back to node 2 with weight 5. If node 2 is already visited when we reach it from node 3, we'd miss examining the edge with weight 5.

Correct Implementation:

def depth_first_search(current_node):
    nonlocal min_score
    for neighbor, edge_weight in graph[current_node]:
        min_score = min(min_score, edge_weight)  # Check ALL edges
        if not visited[neighbor]:
            visited[neighbor] = True
            depth_first_search(neighbor)

Pitfall 2: Forgetting to mark the starting node as visited

Incorrect Implementation:

# Missing: visited[1] = True
depth_first_search(1)

Why it's wrong: Without marking node 1 as visited before starting DFS, if there's a cycle that includes node 1 (e.g., 1β†’2β†’3β†’1), the algorithm will revisit node 1 and potentially cause infinite recursion or stack overflow.

Correct Implementation:

visited[1] = True  # Mark starting node as visited
depth_first_search(1)

Pitfall 3: Misunderstanding the problem - trying to find shortest path

Incorrect Approach: Implementing Dijkstra's algorithm or BFS to find the shortest path from node 1 to node n, then finding the minimum edge in that specific path.

Why it's wrong: The problem allows revisiting nodes and edges multiple times. The optimal solution might use an edge that's not on any shortest path between nodes 1 and n. Since we can traverse freely within the connected component, we need the minimum edge weight in the entire component, not just along a specific path.

Example: If roads = [[1,2,10], [2,3,5], [3,4,20], [1,4,15]], the shortest path from 1 to 4 might be 1β†’4 with total distance 15, but the minimum score is 5 (from the edge 2β†’3), which we can include in our path by going 1β†’2β†’3β†’2β†’1β†’4.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

How would you design a stack which has a function min that returns the minimum element in the stack, in addition to push and pop? All push, pop, min should have running time O(1).


Recommended Readings

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

Load More