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
andn
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 cityn
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.
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:
- Start from city 1
- Explore all reachable cities using DFS
- Track the minimum edge weight encountered during the exploration
- 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 adefaultdict(list)
) to represent the graph - Each city maps to a list of tuples
(neighbor, distance)
for efficient traversal - A visited array
vis
of sizen+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:
- For each city
i
, we examine all its neighborsj
and edge weightsd
- We update
ans
with the minimum edge weight encountered (regardless of whether the neighbor has been visited) - We only recursively visit unvisited neighbors to avoid infinite loops
- The
nonlocal
keyword allows us to modify theans
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 EvaluatorExample 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
- Update
- 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)
- Examine edge (3,2) with weight 6:
- Update
ans = min(6, 6) = 6
- City 2 already visited, skip recursion
- Update
- Return from DFS(3)
- Update
- 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)
- Examine edge (4,2) with weight 5:
- Update
ans = min(5, 5) = 5
- City 2 already visited, skip recursion
- Update
- Examine edge (4,1) with weight 7:
- Update
ans = min(5, 7) = 5
- City 1 already visited, skip recursion
- Update
- Return from DFS(4)
- Update
- Return from DFS(2)
- Update
-
Back in DFS(1), examine edge (1,4) with weight 7:
- Update
ans = min(5, 7) = 5
- City 4 already visited, skip recursion
- Update
-
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 takesO(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 isO(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 requiresO(m)
space. - The visited array
vis
has sizen + 1
, requiringO(n)
space. - The recursion stack for DFS can go up to depth
n
in the worst case (when the graph forms a path), requiringO(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.
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
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Union Find Disjoint Set Union Data Structure Introduction Prerequisite Depth First Search Review problems dfs_intro Once we have a strong grasp of recursion and Depth First Search we can now introduce Disjoint Set Union DSU This data structure is motivated by the following problem Suppose we have sets of elements
Want a Structured Path to Master System Design Too? Donβt Miss This!