3342. Find Minimum Time to Reach Last Room II
Problem Description
You are navigating through a dungeon that consists of n x m
rooms arranged in a grid layout. Your goal is to travel from the starting room at position (0, 0)
to the destination room at position (n - 1, m - 1)
in the minimum possible time.
Each room has a restriction represented by a 2D array moveTime
, where moveTime[i][j]
indicates the earliest time (in seconds) when you can enter room (i, j)
. You begin at room (0, 0)
at time t = 0
.
The movement rules are:
- You can only move to adjacent rooms (rooms that share a wall horizontally or vertically)
- The time cost for moving between adjacent rooms alternates based on the total number of moves you've made:
- Your 1st, 3rd, 5th, ... moves take 1 second each
- Your 2nd, 4th, 6th, ... moves take 2 seconds each
The key constraint is that even if you reach a room early, you cannot enter it until the time specified in moveTime[i][j]
has passed. If you arrive before this time, you must wait until the minimum entry time before entering.
Your task is to find the minimum time required to reach the bottom-right room (n - 1, m - 1)
from the top-left room (0, 0)
, considering both the alternating movement costs and the room entry time restrictions.
Intuition
This problem is essentially a shortest path problem with some special constraints. When we think about finding the shortest path in a grid, Dijkstra's algorithm naturally comes to mind since we're dealing with non-negative weights and need the minimum time.
The key insight is recognizing that the "cost" of moving between rooms isn't constant - it alternates between 1 and 2 seconds. But how do we track which move we're on? A clever observation is that the movement cost pattern depends on the total number of moves made so far. Since we can only move to adjacent cells (up, down, left, right), each move changes our position by exactly 1 in terms of Manhattan distance from the origin.
For any position (i, j)
, the sum (i + j)
tells us the parity of moves taken if we've traveled optimally. If (i + j)
is even, our next move will cost 1 second; if odd, it will cost 2 seconds. This gives us the formula: movement cost = (i + j) % 2 + 1
.
The second constraint is the room entry time restriction. Even if we reach a room early, we might have to wait. So the actual time to enter a room (x, y)
from room (i, j)
is the maximum of:
- The earliest allowed entry time:
moveTime[x][y]
- Our arrival time:
dist[i][j]
+ movement cost
This gives us: t = max(moveTime[x][y], dist[i][j]) + (i + j) % 2 + 1
Since we want the minimum time and different paths might reach the same room at different times, we need to explore all possibilities while keeping track of the best time to reach each room. Dijkstra's algorithm with a priority queue perfectly handles this - it always processes the state with the smallest time first, ensuring we find the optimal solution.
Learn more about Graph, Shortest Path and Heap (Priority Queue) patterns.
Solution Approach
We implement Dijkstra's algorithm to find the shortest path from (0, 0)
to (n-1, m-1)
.
Initialization:
- Create a 2D array
dist
of sizen x m
wheredist[i][j]
represents the minimum time to reach room(i, j)
. Initialize all values to infinity. - Set
dist[0][0] = 0
since we start at room(0, 0)
at time 0. - Create a min-heap priority queue
pq
and add the initial state(0, 0, 0)
representing(time, row, col)
.
Main Algorithm: The algorithm runs in a loop until we find the destination:
-
Extract minimum: Pop the state with minimum time
(d, i, j)
from the priority queue. -
Check destination: If we've reached
(n-1, m-1)
, return the timed
as our answer. -
Skip outdated states: If the current time
d
is greater thandist[i][j]
, skip this state as we've already found a better path to this room. -
Explore neighbors: For each of the four adjacent rooms
(x, y)
:- Check if the position is valid:
0 <= x < n
and0 <= y < m
- Calculate the time to enter room
(x, y)
:
This formula accounts for:t = max(moveTime[x][y], dist[i][j]) + (i + j) % 2 + 1
- Waiting time if we arrive before
moveTime[x][y]
- The alternating movement cost based on position parity
- Waiting time if we arrive before
- Check if the position is valid:
-
Update if better: If
t < dist[x][y]
, we've found a better path:- Update
dist[x][y] = t
- Add
(t, x, y)
to the priority queue
- Update
Direction handling:
The code uses dirs = (-1, 0, 1, 0, -1)
with pairwise
to generate the four directions:
(-1, 0)
for up(0, 1)
for right(1, 0)
for down(0, -1)
for left
The algorithm guarantees finding the minimum time because Dijkstra's algorithm always processes states in order of increasing time, ensuring that when we reach the destination, we've found the optimal path.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the solution approach.
Consider a 3x3 grid with the following moveTime
restrictions:
moveTime = [[0, 4, 6], [1, 3, 5], [2, 2, 4]]
We need to find the minimum time to travel from (0,0) to (2,2).
Step 1: Initialization
- Start at (0,0) at time t=0
- Initialize dist array with infinity, except dist[0][0] = 0
- Priority queue: [(0, 0, 0)] representing (time, row, col)
Step 2: Process (0,0) at time 0 From (0,0), we can move to:
-
(0,1): Position sum = 0+0 = 0 (even), so next move costs 1 second
- Arrival time = 0 + 1 = 1
- Required entry time = moveTime[0][1] = 4
- Actual entry time = max(4, 1) = 4
- Update dist[0][1] = 4, add (4, 0, 1) to queue
-
(1,0): Position sum = 0+0 = 0 (even), so next move costs 1 second
- Arrival time = 0 + 1 = 1
- Required entry time = moveTime[1][0] = 1
- Actual entry time = max(1, 1) = 1
- Update dist[1][0] = 1, add (1, 1, 0) to queue
Priority queue: [(1, 1, 0), (4, 0, 1)]
Step 3: Process (1,0) at time 1 From (1,0), we can move to:
-
(0,0): Already visited with better time, skip
-
(1,1): Position sum = 1+0 = 1 (odd), so next move costs 2 seconds
- Arrival time = 1 + 2 = 3
- Required entry time = moveTime[1][1] = 3
- Actual entry time = max(3, 3) = 3
- Update dist[1][1] = 3, add (3, 1, 1) to queue
-
(2,0): Position sum = 1+0 = 1 (odd), so next move costs 2 seconds
- Arrival time = 1 + 2 = 3
- Required entry time = moveTime[2][0] = 2
- Actual entry time = max(2, 3) = 3
- Update dist[2][0] = 3, add (3, 2, 0) to queue
Priority queue: [(3, 1, 1), (3, 2, 0), (4, 0, 1)]
Step 4: Process (1,1) at time 3 From (1,1), we can move to:
-
(0,1): Position sum = 1+1 = 2 (even), so next move costs 1 second
- Arrival time = 3 + 1 = 4
- Required entry time = moveTime[0][1] = 4
- Actual entry time = max(4, 4) = 4
- dist[0][1] already equals 4, no update needed
-
(1,2): Position sum = 1+1 = 2 (even), so next move costs 1 second
- Arrival time = 3 + 1 = 4
- Required entry time = moveTime[1][2] = 5
- Actual entry time = max(5, 4) = 5
- Update dist[1][2] = 5, add (5, 1, 2) to queue
-
(2,1): Position sum = 1+1 = 2 (even), so next move costs 1 second
- Arrival time = 3 + 1 = 4
- Required entry time = moveTime[2][1] = 2
- Actual entry time = max(2, 4) = 4
- Update dist[2][1] = 4, add (4, 2, 1) to queue
Continuing the process... Eventually, we explore paths to (2,2):
- From (2,1) at time 4: Position sum = 2+1 = 3 (odd), move costs 2 seconds
- Arrival = 4 + 2 = 6, Required = 4, Entry time = 6
- From (1,2) at time 5: Position sum = 1+2 = 3 (odd), move costs 2 seconds
- Arrival = 5 + 2 = 7, Required = 4, Entry time = 7
The minimum time to reach (2,2) is 6 seconds.
This example demonstrates how:
- Movement costs alternate based on position parity
- We must wait if arriving before the allowed entry time
- Dijkstra's algorithm explores all paths to find the minimum
Solution Implementation
1from typing import List
2from heapq import heappush, heappop
3from itertools import pairwise
4from math import inf
5
6class Solution:
7 def minTimeToReach(self, moveTime: List[List[int]]) -> int:
8 # Get grid dimensions
9 rows, cols = len(moveTime), len(moveTime[0])
10
11 # Initialize distance matrix with infinity
12 # dist[i][j] represents minimum time to reach cell (i, j)
13 dist = [[inf] * cols for _ in range(rows)]
14 dist[0][0] = 0
15
16 # Priority queue: (time, row, col)
17 # Start from top-left corner (0, 0) with time 0
18 priority_queue = [(0, 0, 0)]
19
20 # Direction vectors for moving up, right, down, left
21 directions = (-1, 0, 1, 0, -1)
22
23 # Dijkstra's algorithm
24 while True:
25 # Pop the cell with minimum time
26 current_time, row, col = heappop(priority_queue)
27
28 # If we reached the bottom-right corner, return the time
29 if row == rows - 1 and col == cols - 1:
30 return current_time
31
32 # Skip if we've already found a better path to this cell
33 if current_time > dist[row][col]:
34 continue
35
36 # Explore all 4 adjacent cells
37 for dx, dy in pairwise(directions):
38 next_row, next_col = row + dx, col + dy
39
40 # Check if the next cell is within grid bounds
41 if 0 <= next_row < rows and 0 <= next_col < cols:
42 # Calculate time to reach the next cell
43 # Wait until moveTime[next_row][next_col] if necessary
44 # Add alternating movement cost: 1 or 2 based on (row + col) % 2
45 movement_cost = (row + col) % 2 + 1
46 next_time = max(moveTime[next_row][next_col], dist[row][col]) + movement_cost
47
48 # Update if we found a shorter path
49 if dist[next_row][next_col] > next_time:
50 dist[next_row][next_col] = next_time
51 heappush(priority_queue, (next_time, next_row, next_col))
52
1class Solution {
2 public int minTimeToReach(int[][] moveTime) {
3 int numRows = moveTime.length;
4 int numCols = moveTime[0].length;
5
6 // Initialize distance array to track minimum time to reach each cell
7 int[][] minTime = new int[numRows][numCols];
8 for (int[] row : minTime) {
9 Arrays.fill(row, Integer.MAX_VALUE);
10 }
11 minTime[0][0] = 0; // Starting position has 0 time
12
13 // Priority queue to process cells in order of minimum time
14 // Each element: [time, row, col]
15 PriorityQueue<int[]> priorityQueue = new PriorityQueue<>((a, b) -> a[0] - b[0]);
16 priorityQueue.offer(new int[] {0, 0, 0});
17
18 // Direction vectors for moving up, right, down, left
19 int[] directions = {-1, 0, 1, 0, -1};
20
21 // Dijkstra's algorithm to find shortest path
22 while (!priorityQueue.isEmpty()) {
23 int[] current = priorityQueue.poll();
24 int currentTime = current[0];
25 int currentRow = current[1];
26 int currentCol = current[2];
27
28 // Check if we reached the destination
29 if (currentRow == numRows - 1 && currentCol == numCols - 1) {
30 return currentTime;
31 }
32
33 // Skip if we already found a better path to this cell
34 if (currentTime > minTime[currentRow][currentCol]) {
35 continue;
36 }
37
38 // Explore all four adjacent cells
39 for (int dir = 0; dir < 4; dir++) {
40 int nextRow = currentRow + directions[dir];
41 int nextCol = currentCol + directions[dir + 1];
42
43 // Check if the next cell is within bounds
44 if (nextRow >= 0 && nextRow < numRows && nextCol >= 0 && nextCol < numCols) {
45 // Calculate time to reach next cell
46 // Takes the maximum of moveTime constraint and current time
47 // Adds alternating move cost based on Manhattan distance parity
48 int timeToNext = Math.max(moveTime[nextRow][nextCol], minTime[currentRow][currentCol])
49 + ((currentRow + currentCol) % 2) + 1;
50
51 // Update if we found a shorter path
52 if (minTime[nextRow][nextCol] > timeToNext) {
53 minTime[nextRow][nextCol] = timeToNext;
54 priorityQueue.offer(new int[] {timeToNext, nextRow, nextCol});
55 }
56 }
57 }
58 }
59
60 // This line should never be reached given the problem constraints
61 return -1;
62 }
63}
64
1class Solution {
2public:
3 int minTimeToReach(vector<vector<int>>& moveTime) {
4 int rows = moveTime.size();
5 int cols = moveTime[0].size();
6
7 // Initialize distance matrix with maximum values
8 // dist[i][j] represents the minimum time to reach cell (i, j)
9 vector<vector<int>> dist(rows, vector<int>(cols, INT_MAX));
10 dist[0][0] = 0; // Starting position has distance 0
11
12 // Min-heap priority queue: {time, row, col}
13 // Sorted by time in ascending order for Dijkstra's algorithm
14 priority_queue<array<int, 3>, vector<array<int, 3>>, greater<>> minHeap;
15 minHeap.push({0, 0, 0}); // Start from (0, 0) with time 0
16
17 // Direction vectors for moving up, right, down, left
18 int directions[5] = {-1, 0, 1, 0, -1};
19
20 // Dijkstra's algorithm to find shortest path
21 while (!minHeap.empty()) {
22 // Extract cell with minimum time
23 auto [currentTime, currentRow, currentCol] = minHeap.top();
24 minHeap.pop();
25
26 // Check if we reached the destination (bottom-right corner)
27 if (currentRow == rows - 1 && currentCol == cols - 1) {
28 return currentTime;
29 }
30
31 // Skip if we've already found a better path to this cell
32 if (currentTime > dist[currentRow][currentCol]) {
33 continue;
34 }
35
36 // Explore all 4 adjacent cells
37 for (int dir = 0; dir < 4; ++dir) {
38 int nextRow = currentRow + directions[dir];
39 int nextCol = currentCol + directions[dir + 1];
40
41 // Check if the next cell is within bounds
42 if (nextRow >= 0 && nextRow < rows && nextCol >= 0 && nextCol < cols) {
43 // Calculate time to reach the next cell
44 // Must wait until moveTime[nextRow][nextCol] if it's later than current arrival time
45 // Add alternating movement cost: 1 or 2 based on (currentRow + currentCol) % 2
46 int timeToNext = max(moveTime[nextRow][nextCol], dist[currentRow][currentCol])
47 + (currentRow + currentCol) % 2 + 1;
48
49 // Update if we found a better path to the next cell
50 if (dist[nextRow][nextCol] > timeToNext) {
51 dist[nextRow][nextCol] = timeToNext;
52 minHeap.push({timeToNext, nextRow, nextCol});
53 }
54 }
55 }
56 }
57
58 // This line should never be reached if there's always a valid path
59 return -1;
60 }
61};
62
1/**
2 * Finds the minimum time to reach from top-left to bottom-right corner of a grid
3 * @param moveTime - 2D array where moveTime[i][j] represents the earliest time to move to cell (i,j)
4 * @returns Minimum time to reach the destination, or -1 if unreachable
5 */
6function minTimeToReach(moveTime: number[][]): number {
7 const rows = moveTime.length;
8 const cols = moveTime[0].length;
9
10 // Initialize distance array with infinity for all cells
11 const distances: number[][] = Array.from(
12 { length: rows },
13 () => Array(cols).fill(Infinity)
14 );
15 distances[0][0] = 0;
16
17 // Priority queue element type: [time, row, col]
18 type QueueElement = [number, number, number];
19
20 // Initialize priority queue with starting position
21 const priorityQueue = new PriorityQueue<QueueElement>(
22 (a, b) => a[0] - b[0] // Min heap based on time
23 );
24 priorityQueue.enqueue([0, 0, 0]);
25
26 // Direction vectors for moving up, right, down, left
27 const directions = [-1, 0, 1, 0, -1];
28
29 // Process nodes using Dijkstra's algorithm
30 while (!priorityQueue.isEmpty()) {
31 const [currentTime, currentRow, currentCol] = priorityQueue.dequeue();
32
33 // Skip if we've already found a better path to this cell
34 if (currentTime > distances[currentRow][currentCol]) {
35 continue;
36 }
37
38 // Check if we've reached the destination
39 if (currentRow === rows - 1 && currentCol === cols - 1) {
40 return currentTime;
41 }
42
43 // Explore all four adjacent cells
44 for (let dirIndex = 0; dirIndex < 4; dirIndex++) {
45 const nextRow = currentRow + directions[dirIndex];
46 const nextCol = currentCol + directions[dirIndex + 1];
47
48 // Check if the next cell is within bounds
49 if (nextRow >= 0 && nextRow < rows && nextCol >= 0 && nextCol < cols) {
50 // Calculate time to reach the next cell
51 // Wait until moveTime if necessary, then add movement cost
52 // Movement cost alternates based on current position parity
53 const movementCost = ((currentRow + currentCol) % 2) + 1;
54 const nextTime = Math.max(moveTime[nextRow][nextCol], currentTime) + movementCost;
55
56 // Update if we found a better path
57 if (nextTime < distances[nextRow][nextCol]) {
58 distances[nextRow][nextCol] = nextTime;
59 priorityQueue.enqueue([nextTime, nextRow, nextCol]);
60 }
61 }
62 }
63 }
64
65 // Destination unreachable
66 return -1;
67}
68
Time and Space Complexity
Time Complexity: O(n × m × log(n × m))
The algorithm uses Dijkstra's shortest path algorithm with a priority queue (min-heap). In the worst case:
- Each of the
n × m
cells can be visited and added to the priority queue - Each cell can potentially be updated multiple times, but the priority queue ensures we process each cell optimally
- For each cell, we explore up to 4 neighboring cells
- Each heap operation (push and pop) takes
O(log(n × m))
time since the heap can contain at mostn × m
elements - The total number of heap operations is bounded by
O(n × m)
for pops (each cell is popped at most once when it's finalized) andO(n × m × 4)
=O(n × m)
for pushes (each edge is relaxed at most once) - Therefore, the overall time complexity is
O(n × m × log(n × m))
Space Complexity: O(n × m)
The space is used for:
- The
dist
matrix:O(n × m)
to store the minimum distance to each cell - The priority queue
pq
: In the worst case, it can containO(n × m)
elements if multiple paths to cells are discovered before being processed - Other variables like
dirs
and loop variables useO(1)
space - Therefore, the total space complexity is
O(n × m)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Incorrect Movement Cost Calculation
The Problem:
The most common mistake is misunderstanding how the alternating movement cost works. Many developers incorrectly assume the cost alternates based on the current position (row + col) % 2
, but this doesn't track the actual number of moves made.
Why This Happens:
The code uses (row + col) % 2 + 1
to determine movement cost, which gives:
- Even sum positions: cost = 1
- Odd sum positions: cost = 2
This creates a checkerboard pattern that doesn't match the problem's requirement of alternating costs based on move count (1st move = 1 second, 2nd move = 2 seconds, etc.).
The Fix: Track the actual move count in the state:
def minTimeToReach(self, moveTime: List[List[int]]) -> int:
rows, cols = len(moveTime), len(moveTime[0])
# dist[i][j] now stores (min_time, move_count_parity)
dist = [[inf] * cols for _ in range(rows)]
dist[0][0] = 0
# Priority queue: (time, row, col, move_count)
priority_queue = [(0, 0, 0, 0)]
directions = (-1, 0, 1, 0, -1)
while priority_queue:
current_time, row, col, move_count = heappop(priority_queue)
if row == rows - 1 and col == cols - 1:
return current_time
if current_time > dist[row][col]:
continue
for dx, dy in pairwise(directions):
next_row, next_col = row + dx, col + dy
if 0 <= next_row < rows and 0 <= next_col < cols:
# Movement cost based on actual move count
movement_cost = 1 if move_count % 2 == 0 else 2
next_time = max(moveTime[next_row][next_col], current_time) + movement_cost
if dist[next_row][next_col] > next_time:
dist[next_row][next_col] = next_time
heappush(priority_queue, (next_time, next_row, next_col, move_count + 1))
Pitfall 2: Forgetting the Wait Time Logic
The Problem: Another common error is incorrectly handling the waiting constraint. Developers might add the movement cost directly without considering whether they need to wait for the room to become accessible.
Incorrect approach:
next_time = dist[row][col] + movement_cost # Wrong! Ignores moveTime restriction
Correct approach:
# Must wait until moveTime[next_row][next_col] if we arrive early
arrival_time = dist[row][col]
entry_time = max(moveTime[next_row][next_col], arrival_time)
next_time = entry_time + movement_cost
The key insight is that the movement cost is added AFTER we enter the room, not before. If we arrive at a room before its minimum entry time, we wait at the entrance, then the movement to actually enter takes the additional time based on our move count.
What are the most two important steps in writing a depth first search function? (Select 2)
Recommended Readings
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
https assets algo monster cover_photos heap svg Priority Queue and Heap What is the relationship between priority queue and heap Priority Queue is an Abstract Data Type and Heap is the concrete data structure we use to implement a priority queue Priority Queue A priority queue is a data structure
Want a Structured Path to Master System Design Too? Don’t Miss This!