2045. Second Minimum Time to Reach Destination
Problem Description
You have a city represented as an undirected connected graph with n
vertices labeled from 1
to n
. The graph is defined by an edge list edges
, where edges[i] = [ui, vi]
represents a bidirectional edge between vertices ui
and vi
. Each edge takes exactly time
minutes to traverse.
Every vertex has a traffic signal that alternates between green and red. All signals in the city change simultaneously every change
minutes, starting with green when your journey begins. The key rules for movement are:
- You can enter any vertex at any time
- You can only leave a vertex when its signal is green
- You cannot wait at a vertex if the signal is green (you must leave immediately)
Your task is to find the second minimum time to travel from vertex 1
to vertex n
.
The second minimum value is defined as the smallest value that is strictly larger than the minimum value. For example:
- In
[2, 3, 4]
, the second minimum is3
- In
[2, 2, 4]
, the second minimum is4
Important notes:
- You can revisit any vertex multiple times, including the start (vertex
1
) and destination (vertexn
) - At time
0
, all signals have just turned green - The graph guarantees no self-loops and at most one edge between any pair of vertices
The challenge involves finding not just the shortest path, but the second shortest path while accounting for the traffic signal constraints that may force you to wait at certain vertices when signals are red.
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 states we have a graph with
n
vertices and edges connecting them. The city is represented as a bi-directional connected graph.
Is it a tree?
- No: The graph is not a tree because it can have cycles (we can revisit vertices) and there's no mention of it being acyclic or having exactly n-1 edges.
Is the problem related to directed acyclic graphs?
- No: The graph is bidirectional (undirected) and can contain cycles since we can revisit vertices.
Is the problem related to shortest paths?
- Yes: We need to find the second minimum time to reach from vertex 1 to vertex n, which is fundamentally a shortest path variant.
Is the graph Weighted?
- No: While each edge takes
time
minutes to traverse, all edges have the same weight (uniform weight). The complexity comes from traffic signals, not varying edge weights.
Conclusion: The flowchart leads us to BFS (Breadth-First Search). This makes sense because:
- We have an unweighted graph (uniform edge weights)
- We need to find shortest paths (specifically the first and second shortest)
- BFS naturally explores paths level by level, making it ideal for finding the shortest and second-shortest paths
- The solution uses a modified BFS with a queue to track vertices and their distances, maintaining two shortest distances for each vertex
Intuition
The key insight is that we're not just looking for the shortest path, but the second shortest path. In a standard BFS, we typically stop once we reach a vertex for the first time since that guarantees the shortest path. However, for this problem, we need to continue exploring to find a slightly longer path.
Think of it this way: when we reach vertex n
for the first time through BFS, we've found the minimum number of edges needed. But we need to keep searching for the next distinct path length to vertex n
.
The solution maintains two distance values for each vertex: dist[v][0]
for the shortest distance and dist[v][1]
for the second shortest. When we explore from a vertex u
to its neighbor v
:
- If we find a path shorter than the current shortest (
d + 1 < dist[v][0]
), we update the shortest distance - If we find a path that's longer than the shortest but shorter than the current second shortest (
dist[v][0] < d + 1 < dist[v][1]
), we update the second shortest
This modified BFS ensures we capture both the minimum and second minimum number of edges to reach each vertex.
Once we have the second minimum number of edges to vertex n
, we need to calculate the actual time. This is where the traffic signals come in. For each edge we traverse:
- We add
time
minutes for the traversal - If we're not at the last edge and we arrive when the signal is red (determined by
(ans // change) % 2 == 1
), we must wait until it turns green
The signal timing works as follows: signals alternate between green and red every change
minutes. If (total_time // change)
is even, the signal is green; if odd, it's red. When we need to wait at a red signal, we round up to the next green period using (ans + change) // change * change
.
This two-phase approach - first finding the second shortest path length via modified BFS, then calculating the actual time considering traffic signals - gives us the second minimum time to reach the destination.
Learn more about Breadth-First Search, Graph and Shortest Path patterns.
Solution Approach
The implementation uses a modified BFS with specific data structures to track two shortest distances for each vertex:
1. Graph Representation:
g = defaultdict(set)
for u, v in edges:
g[u].add(v)
g[v].add(u)
We build an adjacency list using a defaultdict(set)
to store the bidirectional edges. Sets prevent duplicate edges and provide O(1) lookup.
2. Distance Tracking:
dist = [[inf] * 2 for _ in range(n + 1)]
dist[1][1] = 0
We create a 2D array where dist[v][0]
stores the shortest distance to vertex v
, and dist[v][1]
stores the second shortest. Initially, all distances are set to infinity. We set dist[1][1] = 0
to mark that we start from vertex 1 with 0 edges.
3. Modified BFS:
q = deque([(1, 0)]) while q: u, d = q.popleft() for v in g[u]: if d + 1 < dist[v][0]: dist[v][0] = d + 1 q.append((v, d + 1)) elif dist[v][0] < d + 1 < dist[v][1]: dist[v][1] = d + 1 if v == n: break q.append((v, d + 1))
The BFS queue stores tuples of (vertex, distance)
. For each neighbor v
of current vertex u
:
- If we find a shorter path than the current shortest, we update
dist[v][0]
and continue exploring - If we find a path longer than the shortest but shorter than the second shortest, we update
dist[v][1]
- We can stop early if we've found the second shortest path to vertex
n
4. Time Calculation with Traffic Signals:
ans = 0
for i in range(dist[n][1]):
ans += time
if i < dist[n][1] - 1 and (ans // change) % 2 == 1:
ans = (ans + change) // change * change
After finding the second shortest path with dist[n][1]
edges, we calculate the actual time:
- For each edge traversal, add
time
minutes - After each edge (except the last), check if we arrive during a red signal
- Signal state:
(ans // change) % 2 == 0
means green,== 1
means red - If red, we wait until the next green period:
(ans + change) // change * change
rounds up to the next multiple ofchange
where the signal turns green
The algorithm efficiently finds both the shortest and second shortest paths in O(V + E) time using BFS, then calculates the actual travel time considering traffic signal constraints.
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 small example to illustrate the solution approach.
Example:
- Graph: 4 vertices with edges:
[[1,2], [1,3], [2,4], [3,4]]
time = 3
minutes per edgechange = 5
minutes for signal changes- Find second minimum time from vertex 1 to vertex 4
Step 1: Build Graph
Adjacency list: 1 -> {2, 3} 2 -> {1, 4} 3 -> {1, 4} 4 -> {2, 3}
Step 2: Modified BFS to Find Second Shortest Path
Initialize dist
array (showing only relevant vertices):
dist[1] = [inf, 0] // Start vertex dist[2] = [inf, inf] dist[3] = [inf, inf] dist[4] = [inf, inf]
BFS Process:
-
Start with queue:
[(1, 0)]
-
Process vertex 1 with distance 0:
- Visit neighbor 2:
0 + 1 = 1 < inf
, updatedist[2][0] = 1
, add(2, 1)
to queue - Visit neighbor 3:
0 + 1 = 1 < inf
, updatedist[3][0] = 1
, add(3, 1)
to queue - Queue:
[(2, 1), (3, 1)]
- Visit neighbor 2:
-
Process vertex 2 with distance 1:
- Visit neighbor 1:
1 + 1 = 2
, but we don't need to track back to start - Visit neighbor 4:
1 + 1 = 2 < inf
, updatedist[4][0] = 2
, add(4, 2)
to queue - Queue:
[(3, 1), (4, 2)]
- Visit neighbor 1:
-
Process vertex 3 with distance 1:
- Visit neighbor 1:
1 + 1 = 2
, skip - Visit neighbor 4:
1 + 1 = 2
, already havedist[4][0] = 2
, so check second shortest- Since
2 < 1 + 1 < inf
is false (2 is not less than 2), skip
- Since
- Queue:
[(4, 2)]
- Visit neighbor 1:
-
Process vertex 4 with distance 2:
- Visit neighbor 2:
2 + 1 = 3 > dist[2][0] = 1
and1 < 3 < inf
, updatedist[2][1] = 3
, add(2, 3)
to queue - Visit neighbor 3:
2 + 1 = 3 > dist[3][0] = 1
and1 < 3 < inf
, updatedist[3][1] = 3
, add(3, 3)
to queue - Queue:
[(2, 3), (3, 3)]
- Visit neighbor 2:
-
Process vertex 2 with distance 3:
- Visit neighbor 4:
3 + 1 = 4 > dist[4][0] = 2
and2 < 4 < inf
, updatedist[4][1] = 4
- Since we found second shortest to vertex 4, we can stop
- Visit neighbor 4:
Final distances to vertex 4:
- Shortest path: 2 edges (1→2→4 or 1→3→4)
- Second shortest path: 4 edges (e.g., 1→2→1→3→4)
Step 3: Calculate Time with Traffic Signals
For the second shortest path with 4 edges:
Edge 1 (time 0 to 3):
- Add 3 minutes:
ans = 3
- Check signal:
3 // 5 = 0
,0 % 2 = 0
(green), no wait needed
Edge 2 (time 3 to 6):
- Add 3 minutes:
ans = 6
- Check signal:
6 // 5 = 1
,1 % 2 = 1
(red), must wait - Wait until next green:
(6 + 5) // 5 * 5 = 10
ans = 10
Edge 3 (time 10 to 13):
- Add 3 minutes:
ans = 13
- Check signal:
13 // 5 = 2
,2 % 2 = 0
(green), no wait needed
Edge 4 (time 13 to 16):
- Add 3 minutes:
ans = 16
- This is the last edge, no signal check needed
Result: Second minimum time = 16 minutes
The algorithm successfully finds that while the shortest path takes 2 edges, the second shortest takes 4 edges. With traffic signals causing a wait after the second edge, the total time becomes 16 minutes.
Solution Implementation
1from collections import defaultdict, deque
2from typing import List
3
4class Solution:
5 def secondMinimum(
6 self, n: int, edges: List[List[int]], time: int, change: int
7 ) -> int:
8 # Build adjacency list representation of the graph
9 graph = defaultdict(set)
10 for u, v in edges:
11 graph[u].add(v)
12 graph[v].add(u)
13
14 # Initialize BFS queue with starting node 1 and distance 0
15 queue = deque([(1, 0)])
16
17 # Track two shortest distances for each node
18 # distances[i][0] = shortest distance to node i
19 # distances[i][1] = second shortest distance to node i
20 distances = [[float('inf')] * 2 for _ in range(n + 1)]
21 distances[1][1] = 0 # Starting node has distance 0
22
23 # BFS to find shortest and second shortest paths
24 while queue:
25 current_node, current_distance = queue.popleft()
26
27 # Explore all neighbors
28 for neighbor in graph[current_node]:
29 new_distance = current_distance + 1
30
31 # If we found a shorter path than the current shortest
32 if new_distance < distances[neighbor][0]:
33 distances[neighbor][0] = new_distance
34 queue.append((neighbor, new_distance))
35
36 # If we found a path between shortest and second shortest
37 elif distances[neighbor][0] < new_distance < distances[neighbor][1]:
38 distances[neighbor][1] = new_distance
39 # Early termination if we found second shortest to destination
40 if neighbor == n:
41 break
42 queue.append((neighbor, new_distance))
43
44 # Calculate actual time considering traffic lights
45 total_time = 0
46 second_shortest_path_length = distances[n][1]
47
48 for step in range(second_shortest_path_length):
49 # Add time for one edge traversal
50 total_time += time
51
52 # Check if we need to wait for green light (except for last step)
53 if step < second_shortest_path_length - 1:
54 # If we arrive during red light phase, wait for green
55 if (total_time // change) % 2 == 1:
56 # Round up to next green light period
57 total_time = ((total_time + change) // change) * change
58
59 return total_time
60
1class Solution {
2 public int secondMinimum(int n, int[][] edges, int time, int change) {
3 // Build adjacency list representation of the graph
4 List<Integer>[] graph = new List[n + 1];
5 Arrays.setAll(graph, index -> new ArrayList<>());
6
7 // Add bidirectional edges to the graph
8 for (int[] edge : edges) {
9 int nodeU = edge[0];
10 int nodeV = edge[1];
11 graph[nodeU].add(nodeV);
12 graph[nodeV].add(nodeU);
13 }
14
15 // BFS queue storing [node, distance]
16 Deque<int[]> queue = new LinkedList<>();
17 queue.offerLast(new int[] {1, 0});
18
19 // distances[node][0] = shortest distance, distances[node][1] = second shortest distance
20 int[][] distances = new int[n + 1][2];
21 for (int i = 0; i < n + 1; ++i) {
22 Arrays.fill(distances[i], Integer.MAX_VALUE);
23 }
24 distances[1][1] = 0; // Initialize source node's second shortest distance
25
26 // BFS to find shortest and second shortest paths
27 while (!queue.isEmpty()) {
28 int[] current = queue.pollFirst();
29 int currentNode = current[0];
30 int currentDistance = current[1];
31
32 // Explore all neighbors
33 for (int neighbor : graph[currentNode]) {
34 int newDistance = currentDistance + 1;
35
36 // If we found a shorter path than the current shortest
37 if (newDistance < distances[neighbor][0]) {
38 distances[neighbor][0] = newDistance;
39 queue.offerLast(new int[] {neighbor, newDistance});
40 }
41 // If we found a path longer than shortest but shorter than second shortest
42 else if (distances[neighbor][0] < newDistance && newDistance < distances[neighbor][1]) {
43 distances[neighbor][1] = newDistance;
44
45 // Early termination if we reached the destination
46 if (neighbor == n) {
47 break;
48 }
49 queue.offerLast(new int[] {neighbor, newDistance});
50 }
51 }
52 }
53
54 // Calculate actual travel time considering traffic lights
55 int totalTime = 0;
56 int secondShortestPathLength = distances[n][1];
57
58 for (int step = 0; step < secondShortestPathLength; ++step) {
59 // Add time for one edge traversal
60 totalTime += time;
61
62 // Check if we need to wait for green light (except for the last step)
63 if (step < secondShortestPathLength - 1) {
64 // If current signal cycle is red (odd), wait for next green
65 if ((totalTime / change) % 2 == 1) {
66 totalTime = (totalTime + change) / change * change;
67 }
68 }
69 }
70
71 return totalTime;
72 }
73}
74
1class Solution {
2public:
3 int secondMinimum(int n, vector<vector<int>>& edges, int time, int change) {
4 // Build adjacency list representation of the graph
5 vector<vector<int>> graph(n + 1);
6
7 // Add bidirectional edges to the graph
8 for (const auto& edge : edges) {
9 int node_u = edge[0];
10 int node_v = edge[1];
11 graph[node_u].push_back(node_v);
12 graph[node_v].push_back(node_u);
13 }
14
15 // BFS queue storing pairs of [node, distance]
16 queue<pair<int, int>> bfs_queue;
17 bfs_queue.push({1, 0});
18
19 // distances[node][0] = shortest distance, distances[node][1] = second shortest distance
20 vector<vector<int>> distances(n + 1, vector<int>(2, INT_MAX));
21 distances[1][0] = 0; // Initialize source node's shortest distance
22
23 // BFS to find shortest and second shortest paths
24 while (!bfs_queue.empty()) {
25 auto [current_node, current_distance] = bfs_queue.front();
26 bfs_queue.pop();
27
28 // Explore all neighbors
29 for (int neighbor : graph[current_node]) {
30 int new_distance = current_distance + 1;
31
32 // If we found a shorter path than the current shortest
33 if (new_distance < distances[neighbor][0]) {
34 distances[neighbor][0] = new_distance;
35 bfs_queue.push({neighbor, new_distance});
36 }
37 // If we found a path longer than shortest but shorter than second shortest
38 else if (distances[neighbor][0] < new_distance && new_distance < distances[neighbor][1]) {
39 distances[neighbor][1] = new_distance;
40
41 // Early termination if we reached the destination
42 if (neighbor == n) {
43 break;
44 }
45 bfs_queue.push({neighbor, new_distance});
46 }
47 }
48 }
49
50 // Calculate actual travel time considering traffic lights
51 int total_time = 0;
52 int second_shortest_path_length = distances[n][1];
53
54 for (int step = 0; step < second_shortest_path_length; ++step) {
55 // Add time for one edge traversal
56 total_time += time;
57
58 // Check if we need to wait for green light (except for the last step)
59 if (step < second_shortest_path_length - 1) {
60 // If current signal cycle is red (odd), wait for next green
61 if ((total_time / change) % 2 == 1) {
62 total_time = (total_time + change) / change * change;
63 }
64 }
65 }
66
67 return total_time;
68 }
69};
70
1function secondMinimum(n: number, edges: number[][], time: number, change: number): number {
2 // Build adjacency list representation of the graph
3 const graph: number[][] = Array(n + 1).fill(null).map(() => []);
4
5 // Add bidirectional edges to the graph
6 for (const edge of edges) {
7 const nodeU = edge[0];
8 const nodeV = edge[1];
9 graph[nodeU].push(nodeV);
10 graph[nodeV].push(nodeU);
11 }
12
13 // BFS queue storing [node, distance]
14 const queue: number[][] = [];
15 queue.push([1, 0]);
16
17 // distances[node][0] = shortest distance, distances[node][1] = second shortest distance
18 const distances: number[][] = Array(n + 1).fill(null).map(() => [Infinity, Infinity]);
19 distances[1][1] = 0; // Initialize source node's second shortest distance
20
21 // BFS to find shortest and second shortest paths
22 while (queue.length > 0) {
23 const current = queue.shift()!;
24 const currentNode = current[0];
25 const currentDistance = current[1];
26
27 // Explore all neighbors
28 for (const neighbor of graph[currentNode]) {
29 const newDistance = currentDistance + 1;
30
31 // If we found a shorter path than the current shortest
32 if (newDistance < distances[neighbor][0]) {
33 distances[neighbor][0] = newDistance;
34 queue.push([neighbor, newDistance]);
35 }
36 // If we found a path longer than shortest but shorter than second shortest
37 else if (distances[neighbor][0] < newDistance && newDistance < distances[neighbor][1]) {
38 distances[neighbor][1] = newDistance;
39
40 // Early termination if we reached the destination
41 if (neighbor === n) {
42 break;
43 }
44 queue.push([neighbor, newDistance]);
45 }
46 }
47 }
48
49 // Calculate actual travel time considering traffic lights
50 let totalTime = 0;
51 const secondShortestPathLength = distances[n][1];
52
53 for (let step = 0; step < secondShortestPathLength; step++) {
54 // Add time for one edge traversal
55 totalTime += time;
56
57 // Check if we need to wait for green light (except for the last step)
58 if (step < secondShortestPathLength - 1) {
59 // If current signal cycle is red (odd), wait for next green
60 if (Math.floor(totalTime / change) % 2 === 1) {
61 totalTime = Math.floor((totalTime + change) / change) * change;
62 }
63 }
64 }
65
66 return totalTime;
67}
68
Time and Space Complexity
Time Complexity: O(V + E)
where V
is the number of vertices (n) and E
is the number of edges.
- Building the adjacency list takes
O(E)
time as we iterate through all edges once. - The BFS traversal visits each vertex at most twice (once for the shortest path and once for the second shortest path).
- Each edge is explored at most twice from each direction, resulting in
O(E)
edge explorations. - The final loop to calculate the actual time runs
dist[n][1]
times, which is at mostO(V)
since the second shortest path length is bounded by the number of vertices. - Overall:
O(V + E)
for graph traversal +O(V)
for time calculation =O(V + E)
Space Complexity: O(V + E)
- The adjacency list
g
stores all edges, requiringO(E)
space. - The
dist
array is a 2D array of size(n+1) × 2
, requiringO(V)
space. - The BFS queue can contain at most
O(V)
elements in the worst case, as each vertex can be added at most twice. - Overall:
O(E)
for adjacency list +O(V)
for distance array +O(V)
for queue =O(V + E)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Initial Distance Setting
A critical pitfall in this solution is the initialization of distances[1][1] = 0
instead of distances[1][0] = 0
. This means we're marking the second shortest distance to the starting node as 0, not the shortest distance.
Why it's problematic:
- The BFS logic expects
distances[v][0]
to contain the shortest distance - With
distances[1][1] = 0
anddistances[1][0] = inf
, the algorithm treats infinity as the shortest path from node 1 - This causes incorrect comparisons when processing neighbors of node 1
Solution:
distances[1][0] = 0 # Correct: shortest distance to start is 0
2. Missing Path Validation for Second Shortest
The code doesn't ensure that the second shortest path is actually different from the shortest path. In graphs where multiple paths of the same length exist, we might incorrectly identify a path of the same length as the "second shortest."
Why it's problematic:
- If there are multiple shortest paths with the same number of edges, the second shortest should be strictly longer
- The current code might return the same distance twice if not careful
Solution: Add explicit checking to ensure the second shortest is strictly greater:
# When updating second shortest distance elif distances[neighbor][0] < new_distance < distances[neighbor][1]: # Only update if this creates a truly different path length if new_distance > distances[neighbor][0]: # Ensure strictly greater distances[neighbor][1] = new_distance
3. Premature Termination
The early break when finding the second shortest path to node n might cause issues:
if neighbor == n: break # This breaks from the inner loop, not the while loop
Why it's problematic:
- This only breaks from the inner for loop, not the entire BFS
- Could lead to incomplete exploration of other potentially important paths
- The queue might still contain unprocessed nodes that could lead to better solutions
Solution: Use a flag or different termination logic:
found_second = False while queue and not found_second: current_node, current_distance = queue.popleft() for neighbor in graph[current_node]: # ... existing logic ... elif distances[neighbor][0] < new_distance < distances[neighbor][1]: distances[neighbor][1] = new_distance if neighbor == n: found_second = True break queue.append((neighbor, new_distance))
4. Integer Division Timing Issue
The traffic light waiting calculation could have edge cases with integer division:
total_time = ((total_time + change) // change) * change
Why it's problematic:
- If
total_time
is already at a change boundary, this formula still rounds up - For example, if
total_time = 10
andchange = 5
, and we're in red phase, we should wait until15
, but the formula might not handle boundary cases correctly
Solution: Be more explicit about the waiting logic:
if (total_time // change) % 2 == 1: # Red light # Calculate next green light start time current_period = total_time // change next_green_period = current_period + 1 if current_period % 2 == 1 else current_period + 2 total_time = next_green_period * change
What are the two properties the problem needs to have for dynamic programming to be applicable? (Select 2)
Recommended Readings
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
https assets algo monster cover_photos graph svg Graph Fundamentals Tree with 0 cycle At this point you should be pretty familiar with trees A tree is a special kind of graph a connected acyclic cycle less graph A graph may contain cycle s and nodes could be disconnected A tree
Shortest Path Between A and B Prereq BFS on Graph problems graph_bfs Given an unweighted connected graph return the length of the shortest path between two nodes A and B in terms of the number of edges Assume there always exists a path between nodes A and B Input graph
Want a Structured Path to Master System Design Too? Don’t Miss This!