Minimum Score of a Path Between Two Cities

Medium DFS

Problem

Given n cities numbered from 1 to n and a list of roads where each road connects two cities with a certain score, return the minimum score of any path between city 1 and city n. You may assume the graph is connected and the path can revisit cities and roads multiple times.

  • 2 ≤ n ≤ 10⁵
  • 1 ≤ roads.length ≤ 10⁵
  • roads[i].length == 3
  • 1 ≤ a_i, b_i ≤ n
  • 1 ≤ score_i ≤ 10⁴
  • There is at least one path between city 1 and city n

Example

Input: n = 4, roads = [[1,2,9],[2,3,6],[2,4,5],[1,4,7]]
Output: 5

The graph has edges with scores 9, 6, 5, and 7. The path from city 1 to city 4 can be direct with score 7, or via city 2 and 4 with edges 9 and 5. The minimum score along the path 1->2->4 is min(9,5) = 5, which is less than the direct edge 7. The algorithm uses DFS starting from city 1 to explore all reachable nodes, tracking the minimum edge score encountered. The critical moment is when the DFS visits edge (2,4) with score 5, updating the global minimum score to 5. Since the graph is connected, DFS visits all nodes reachable from city 1, ensuring the minimum edge score on any path to city n is found.

Approach

Straightforward Solution

A brute-force approach would enumerate all paths from city 1 to city n and compute the minimum edge score for each, then take the minimum among these. This is computationally infeasible due to exponential path counts.

Core Observation

The minimum score of a path between two cities is the smallest edge weight on any path connecting them. Since the graph is connected, all nodes reachable from city 1 can be explored, and the minimum edge score among all edges in the connected component containing city 1 is the answer.

Path to Optimal

Recognizing that the minimum edge score on any path from city 1 to city n must be at least the minimum edge in the connected component of city 1, the problem reduces to finding the minimum edge weight in that component. DFS or BFS can traverse the connected component efficiently, updating the minimum edge score encountered. This avoids enumerating all paths and achieves linear time complexity.

Optimal Approach

Build an adjacency list representation of the graph. Use DFS starting from city 1 to visit all reachable nodes. During traversal, track the minimum edge score encountered. Since the graph is connected, the DFS will reach city n, and the global minimum edge score found during traversal is the minimum score of any path between city 1 and city n.

Time

O(n + m)

Building the adjacency list takes O(m) time, where m is the number of roads. DFS visits each node and edge once, resulting in O(n + m) total time.

Space

O(n + m)

The adjacency list requires O(n + m) space. The recursion stack in DFS can go up to O(n) in the worst case. The visited set also uses O(n) space. This auxiliary space is necessary to track visited nodes and graph structure.

Pattern Spotlight

DFS (Graph Connectivity and State Propagation)

When the problem requires aggregating a property (like minimum edge weight) over all nodes reachable from a source in an undirected graph, a DFS traversal that propagates and updates a global state efficiently captures the solution without enumerating all paths.

Solution

1class Solution:
2 def minScore(self, n: int, roads: List[List[int]]) -> int:
3 graph = defaultdict(list)
4
5 for a, b, w in roads:
6 graph[a].append((b, w))
7 graph[b].append((a, w))
8
9 visited = set()
10 res = float("inf")
11
12 def dfs(node):
13 nonlocal res
14 visited.add(node)
15
16 for nei, w in graph[node]:
17 res = min(res, w)
18 if nei not in visited:
19 dfs(nei)
20
21 dfs(1)
22 return res

Step-by-Step Solution

1

Construct the Undirected Graph as an Adjacency List

3graph = defaultdict(list)
5for a, b, w in roads:
6 graph[a].append((b, w))
7 graph[b].append((a, w))

Objective

To represent the road network efficiently for traversal by building an adjacency list mapping each city to its neighbors and edge scores.

Key Insight

An adjacency list allows O(1) average-time access to neighbors of any node and is space-efficient for sparse graphs. Representing the graph as undirected requires adding each edge in both directions, ensuring traversal can proceed from any node to its connected neighbors.

Interview Quick-Check

Core Logic

Building the adjacency list with bidirectional edges ensures the graph accurately models the undirected road network.

Common Pitfalls & Bugs

Forgetting to add edges in both directions leads to incomplete traversal and incorrect results.

2

Initialize Visited Set and Result Variable

9visited = set()
10res = float("inf")

Objective

To prepare tracking structures for DFS traversal and to store the minimum edge score found during exploration.

Key Insight

The visited set prevents revisiting nodes and infinite recursion. Initializing the result to infinity ensures any edge score encountered will update it to a smaller value, correctly tracking the global minimum.

Interview Quick-Check

Core Logic

The visited set guarantees each node is processed once, preventing cycles and redundant work.

State & Boundaries

Initializing the result to infinity sets a correct baseline for minimum comparisons.

3

Traverse Graph with DFS to Update Minimum Edge Score

12def dfs(node):
13 nonlocal res
14 visited.add(node)
16 for nei, w in graph[node]:
17 res = min(res, w)
18 if nei not in visited:
19 dfs(nei)

Objective

To recursively explore all nodes reachable from city 1, updating the global minimum edge score with each edge encountered.

Key Insight

DFS systematically visits every node and edge in the connected component. By updating the minimum edge score at each edge, the algorithm accumulates the smallest edge weight in the entire component. The use of a nonlocal variable allows the recursive calls to share and update this global state efficiently.

Interview Quick-Check

Core Logic

DFS explores all reachable nodes and edges, updating the global minimum edge score to reflect the smallest edge in the connected component.

State & Boundaries

The visited set prevents revisiting nodes, ensuring termination and correctness.

Common Pitfalls & Bugs

Failing to update the minimum edge score before recursive calls can miss smaller edges deeper in the graph.

4

Invoke DFS from City 1 and Return the Minimum Score

21dfs(1)
22return res

Objective

To start the DFS traversal from city 1 and return the minimum edge score found after full exploration.

Key Insight

Starting DFS at city 1 ensures the entire connected component containing city 1 is explored. Since the graph is connected, city n is guaranteed to be reachable, and the minimum edge score found during traversal is the answer. Returning the global minimum after traversal completes provides the final solution.

Interview Quick-Check

Core Logic

Starting DFS at city 1 ensures all nodes reachable from city 1, including city n, are visited.

State & Boundaries

Returning the global minimum after traversal completes gives the minimum score of any path from city 1 to city n.

Line Analysis

This solution has 3 Critical lines interviewers watch for.

Line 17 Critical
res = min(res, w)

Update the global minimum edge score with the current edge weight.

This line is the core of the algorithm: it continuously tracks the smallest edge weight encountered during traversal, which directly determines the minimum score of any path between city 1 and city n.

Line 13 Critical
nonlocal res

Declare res as nonlocal to update global minimum edge score.

Using nonlocal allows the recursive DFS calls to update the shared minimum edge score variable, maintaining state across recursion levels.

Line 21 Critical
dfs(1)

Start DFS traversal from city 1.

Initiating DFS at city 1 guarantees exploration of all nodes reachable from city 1, including city n, ensuring the minimum edge score is found.

Line 3 Core
graph = defaultdict(list)

Initialize an adjacency list to represent the graph.

Using a defaultdict of lists provides an efficient and flexible structure to store neighbors and edge weights for each city, enabling quick lookups during traversal.

Line 6 Core
graph[a].append((b, w))

Add neighbor and edge weight for city a.

Adding (b, w) to city a's adjacency list models the undirected edge from a to b with weight w.

Line 7 Core
graph[b].append((a, w))

Add neighbor and edge weight for city b.

Adding (a, w) to city b's adjacency list completes the undirected edge representation, allowing traversal from b to a.

Line 9 Core
visited = set()

Create a set to track visited cities during DFS.

The visited set prevents revisiting nodes, avoiding infinite recursion and redundant computations.

Line 14 Core
visited.add(node)

Mark the current node as visited.

Adding the node to visited prevents revisiting it, ensuring the DFS terminates and does not process nodes multiple times.

Line 18 Core
if nei not in visited:

Check if the neighbor has not been visited.

This condition prevents revisiting nodes, avoiding infinite recursion and redundant exploration.

Line 19 Core
dfs(nei)

Recursively call DFS on the unvisited neighbor.

Recursing into unvisited neighbors ensures the entire connected component is explored, allowing the minimum edge score to be updated with all edges.

Line 22 Core
return res

Return the minimum edge score found after traversal.

Returning the global minimum edge score after DFS completes provides the final answer to the problem.

Line 5 Core
for a, b, w in roads:

Iterate over each road to build the graph.

Processing each road allows the construction of the undirected graph by adding edges in both directions, ensuring connectivity representation.

Line 10 Core
res = float("inf")

Initialize the result variable to infinity.

Setting the initial minimum edge score to infinity ensures any encountered edge weight will update it correctly to the smallest value.

Line 12 Core
def dfs(node):

Define the DFS function to explore the graph.

Encapsulating the traversal logic in a recursive function allows systematic exploration of all reachable nodes and edges.

Line 16 Core
for nei, w in graph[node]:

Iterate over neighbors and edge weights of the current node.

Exploring all adjacent nodes and their edge weights is essential to traverse the entire connected component and update the minimum edge score.

Test Your Understanding

Why does traversing the connected component of city 1 and tracking the minimum edge score guarantee the minimum score of any path to city n?

Because any path from city 1 to city n must lie entirely within the connected component of city 1, and the minimum edge score on any such path cannot be less than the smallest edge in that component. DFS visits all nodes and edges in this component, ensuring the global minimum edge score found is the minimum possible on any path.

Related Problems

DFS pattern

Don't just read it. Drill it.

Reconstruct Minimum Score of a Path Between Two Cities from memory until it sticks. AlgoDrill blanks out key lines and makes you fill them back in, step by step.

Unlock the Minimum Score of a Path Between Two Cities drill

or drill a free problem