Facebook Pixel

3341. Find Minimum Time to Reach Last Room I

Problem Description

You are given a dungeon represented as an n x m grid of rooms. Each room has a specific opening time, and you need to find the minimum time to travel from the top-left corner to the bottom-right corner.

The problem provides:

  • A 2D array moveTime of size n x m, where moveTime[i][j] represents the minimum time (in seconds) after which room (i, j) becomes accessible
  • You start at room (0, 0) at time t = 0
  • You can only move to adjacent rooms (horizontally or vertically, not diagonally)
  • Moving from one room to an adjacent room takes exactly 1 second

The key constraint is that you cannot enter a room until it opens. For example, if moveTime[1][0] = 5, you cannot enter room (1, 0) until at least 5 seconds have passed since the start.

The movement rules are:

  1. You can only move to a room if the current time is at least equal to that room's opening time
  2. Each move to an adjacent room takes 1 second
  3. If you arrive at a room before it opens, you must wait until it opens before entering

Your goal is to find the minimum time needed to reach the destination room (n-1, m-1) from the starting room (0, 0).

The solution uses Dijkstra's algorithm to find the shortest path, where the "distance" is the time needed to reach each room. For each room, the algorithm calculates the earliest possible arrival time by considering both the travel time and the room's opening time constraint.

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

Intuition

This problem is essentially a shortest path problem with a twist - we have time constraints on when we can enter each room. This immediately suggests using a graph traversal algorithm, but we need to handle the timing constraints carefully.

The key insight is that when we arrive at a room, the actual time we can enter it is the maximum of:

  1. Our arrival time (based on our current path)
  2. The room's opening time (moveTime[x][y])

For example, if we arrive at room (x, y) at time 10, but the room doesn't open until time 15, we must wait until time 15 to enter. Then moving to the next room takes 1 additional second, so we'd be at the next room at time 16.

This formula can be expressed as: t = max(moveTime[x][y], current_time) + 1

Why Dijkstra's algorithm? Because we're looking for the minimum time (which is analogous to shortest distance), and we need to explore paths in order of increasing time. The greedy nature of Dijkstra's ensures that when we first reach the destination, we've found the optimal path.

The algorithm maintains a priority queue sorted by time, always processing the room we can reach earliest. This guarantees that:

  • We explore rooms in chronological order
  • The first time we reach the destination is the optimal time
  • We avoid revisiting rooms with worse times (the dist array tracks the best time to reach each room)

The waiting constraint doesn't break Dijkstra's optimality because waiting at a room doesn't create negative edge weights - it only increases the total time, maintaining the algorithm's assumptions.

Learn more about Graph, Shortest Path and Heap (Priority Queue) patterns.

Solution Approach

The solution implements Dijkstra's algorithm with a priority queue to find the minimum time path. Let's walk through the implementation step by step:

1. Initialization

n, m = len(moveTime), len(moveTime[0])
dist = [[inf] * m for _ in range(n)]
dist[0][0] = 0
pq = [(0, 0, 0)]
  • Create a dist array to track the minimum time to reach each room, initialized to infinity
  • Set dist[0][0] = 0 since we start at room (0, 0) at time 0
  • Initialize a priority queue with the starting state (0, 0, 0) representing (time, row, col)

2. Direction Setup

dirs = (-1, 0, 1, 0, -1)

This clever pattern allows us to iterate through the four directions (up, right, down, left) using pairwise(dirs), which generates pairs like (-1, 0), (0, 1), (1, 0), (0, -1).

3. Main Dijkstra's Loop

while 1:
    d, i, j = heappop(pq)
    if i == n - 1 and j == m - 1:
        return d
  • Extract the state with minimum time from the priority queue
  • If we've reached the destination (n-1, m-1), return the time immediately

4. Skip Outdated States

if d > dist[i][j]:
    continue

Since we might add multiple states for the same room to the priority queue, skip this state if we've already found a better path to this room.

5. Explore Adjacent Rooms

for a, b in pairwise(dirs):
    x, y = i + a, j + b
    if 0 <= x < n and 0 <= y < m:
        t = max(moveTime[x][y], dist[i][j]) + 1
        if dist[x][y] > t:
            dist[x][y] = t
            heappush(pq, (t, x, y))

For each valid adjacent room (x, y):

  • Calculate the arrival time: t = max(moveTime[x][y], dist[i][j]) + 1
    • dist[i][j] is when we're ready to leave room (i, j)
    • moveTime[x][y] is when room (x, y) opens
    • We take the maximum to handle waiting, then add 1 for the movement
  • If this time t is better than the previously known time dist[x][y], update it and add the new state to the priority queue

The algorithm guarantees that when we first reach the destination, we've found the optimal path because we always process states in order of increasing time.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a small example to illustrate the solution approach.

Consider a 3×3 grid where moveTime is:

moveTime = [[0, 4, 6],
            [1, 8, 2],
            [3, 1, 5]]

We need to find the minimum time to travel from (0,0) to (2,2).

Step 1: Initialization

  • Start at room (0,0) at time 0
  • Priority queue: [(0, 0, 0)]
  • Distance array initialized with infinity except dist[0][0] = 0

Step 2: Process (0,0) at time 0

  • Current position: (0,0), current time: 0
  • Check adjacent rooms:
    • Room (0,1): Opens at time 4, we're at time 0
      • Time to enter: max(4, 0) + 1 = 5
      • Add to queue: (5, 0, 1)
    • Room (1,0): Opens at time 1, we're at time 0
      • Time to enter: max(1, 0) + 1 = 2
      • Add to queue: (2, 1, 0)

Priority queue now: [(2, 1, 0), (5, 0, 1)]

Step 3: Process (1,0) at time 2

  • Current position: (1,0), current time: 2
  • Check adjacent rooms:
    • Room (0,0): Already visited with better time
    • Room (1,1): Opens at time 8, we're at time 2
      • Time to enter: max(8, 2) + 1 = 9
      • Add to queue: (9, 1, 1)
    • Room (2,0): Opens at time 3, we're at time 2
      • Time to enter: max(3, 2) + 1 = 4
      • Add to queue: (4, 2, 0)

Priority queue now: [(4, 2, 0), (5, 0, 1), (9, 1, 1)]

Step 4: Process (2,0) at time 4

  • Current position: (2,0), current time: 4
  • Check adjacent rooms:
    • Room (1,0): Already visited with better time
    • Room (2,1): Opens at time 1, we're at time 4
      • Time to enter: max(1, 4) + 1 = 5
      • Add to queue: (5, 2, 1)

Priority queue now: [(5, 0, 1), (5, 2, 1), (9, 1, 1)]

Step 5: Process (0,1) at time 5

  • Current position: (0,1), current time: 5
  • Check adjacent rooms:
    • Room (0,0): Already visited with better time
    • Room (0,2): Opens at time 6, we're at time 5
      • Time to enter: max(6, 5) + 1 = 7
      • Add to queue: (7, 0, 2)
    • Room (1,1): Opens at time 8, we're at time 5
      • Time to enter: max(8, 5) + 1 = 9 (no improvement)

Step 6: Process (2,1) at time 5

  • Current position: (2,1), current time: 5
  • Check adjacent rooms:
    • Room (2,0): Already visited with better time
    • Room (2,2): Opens at time 5, we're at time 5
      • Time to enter: max(5, 5) + 1 = 6
      • Add to queue: (6, 2, 2)
    • Room (1,1): Opens at time 8, we're at time 5
      • Time to enter: max(8, 5) + 1 = 9 (no improvement)

Priority queue now: [(6, 2, 2), (7, 0, 2), (9, 1, 1)]

Step 7: Process (2,2) at time 6

  • We've reached the destination!
  • Return 6 as the minimum time

The optimal path was: (0,0) → (1,0) → (2,0) → (2,1) → (2,2), taking 6 seconds total. Note how we had to wait at room (1,0) since we arrived at time 2 but couldn't enter (2,0) until time 3.

Solution Implementation

1from heapq import heappush, heappop
2from itertools import pairwise
3from math import inf
4from typing import List
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 the minimum time to reach cell (i, j)
13        dist = [[inf] * cols for _ in range(rows)]
14        dist[0][0] = 0  # Starting position has distance 0
15      
16        # Priority queue for Dijkstra's algorithm
17        # Format: (time, row, col)
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        # Process nodes using Dijkstra's algorithm
24        while priority_queue:
25            current_time, current_row, current_col = heappop(priority_queue)
26          
27            # Check if we've reached the destination (bottom-right corner)
28            if current_row == rows - 1 and current_col == cols - 1:
29                return current_time
30          
31            # Skip if we've already found a better path to this cell
32            if current_time > dist[current_row][current_col]:
33                continue
34          
35            # Explore all four adjacent cells
36            for delta_row, delta_col in pairwise(directions):
37                next_row = current_row + delta_row
38                next_col = current_col + delta_col
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                    # Must wait until moveTime[next_row][next_col] if needed, then add 1 for the move
44                    next_time = max(moveTime[next_row][next_col], dist[current_row][current_col]) + 1
45                  
46                    # Update if we found a better path to the next cell
47                    if dist[next_row][next_col] > next_time:
48                        dist[next_row][next_col] = next_time
49                        heappush(priority_queue, (next_time, next_row, next_col))
50
1class Solution {
2    public int minTimeToReach(int[][] moveTime) {
3        // Get grid dimensions
4        int rows = moveTime.length;
5        int cols = moveTime[0].length;
6      
7        // Initialize distance array to track minimum time to reach each cell
8        int[][] minTime = new int[rows][cols];
9        for (int[] row : minTime) {
10            Arrays.fill(row, Integer.MAX_VALUE);
11        }
12        minTime[0][0] = 0; // Starting position has 0 time
13      
14        // Priority queue to process cells by minimum time (Dijkstra's algorithm)
15        // Each element: [time, row, col]
16        PriorityQueue<int[]> priorityQueue = new PriorityQueue<>((a, b) -> Integer.compare(a[0], b[0]));
17        priorityQueue.offer(new int[] {0, 0, 0});
18      
19        // Direction vectors for moving up, right, down, left
20        int[] directions = {-1, 0, 1, 0, -1};
21      
22        // Process cells until we reach the destination
23        while (!priorityQueue.isEmpty()) {
24            int[] current = priorityQueue.poll();
25            int currentTime = current[0];
26            int currentRow = current[1];
27            int currentCol = current[2];
28          
29            // Check if we've reached the destination (bottom-right corner)
30            if (currentRow == rows - 1 && currentCol == cols - 1) {
31                return currentTime;
32            }
33          
34            // Skip if we've already found a better path to this cell
35            if (currentTime > minTime[currentRow][currentCol]) {
36                continue;
37            }
38          
39            // Explore all 4 adjacent cells
40            for (int dir = 0; dir < 4; dir++) {
41                int nextRow = currentRow + directions[dir];
42                int nextCol = currentCol + directions[dir + 1];
43              
44                // Check if the next cell is within grid bounds
45                if (nextRow >= 0 && nextRow < rows && nextCol >= 0 && nextCol < cols) {
46                    // Calculate time to reach the next cell
47                    // Must wait until moveTime[nextRow][nextCol] if it's greater than current time
48                    // Then add 1 for the move itself
49                    int timeToNext = Math.max(moveTime[nextRow][nextCol], minTime[currentRow][currentCol]) + 1;
50                  
51                    // Update if we found a faster path to the next cell
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 if the grid is valid
61        return -1;
62    }
63}
64
1class Solution {
2public:
3    int minTimeToReach(vector<vector<int>>& moveTime) {
4        // Get grid dimensions
5        int rows = moveTime.size();
6        int cols = moveTime[0].size();
7      
8        // Initialize distance matrix to track minimum time to reach each cell
9        vector<vector<int>> minTime(rows, vector<int>(cols, INT_MAX));
10        minTime[0][0] = 0;  // Starting position has time 0
11      
12        // Min-heap priority queue: {time, row, col}
13        // Ordered by time (smallest first)
14        priority_queue<array<int, 3>, vector<array<int, 3>>, greater<>> minHeap;
15        minHeap.push({0, 0, 0});  // Start from top-left corner
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 (true) {
22            // Extract cell with minimum time
23            auto [currentTime, currentRow, currentCol] = minHeap.top();
24            minHeap.pop();
25          
26            // Check if we've 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 > minTime[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 grid boundaries
42                if (nextRow >= 0 && nextRow < rows && 
43                    nextCol >= 0 && nextCol < cols) {
44                  
45                    // Calculate time to reach the next cell
46                    // Must wait until moveTime[nextRow][nextCol] if necessary
47                    // Then add 1 for the move itself
48                    int timeToNext = max(moveTime[nextRow][nextCol], 
49                                       minTime[currentRow][currentCol]) + 1;
50                  
51                    // Update if we found a shorter path
52                    if (minTime[nextRow][nextCol] > timeToNext) {
53                        minTime[nextRow][nextCol] = timeToNext;
54                        minHeap.push({timeToNext, nextRow, nextCol});
55                    }
56                }
57            }
58        }
59    }
60};
61
1/**
2 * Finds the minimum time to reach from top-left to bottom-right corner of the 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({ length: rows }, () => Array(cols).fill(Infinity));
12    distances[0][0] = 0; // Starting position has distance 0
13  
14    // Priority queue to store [time, row, col] tuples, sorted by time
15    type QueueNode = [number, number, number];
16    const priorityQueue = new PriorityQueue<QueueNode>((a, b) => a[0] - b[0]);
17    priorityQueue.enqueue([0, 0, 0]); // Start from top-left corner at time 0
18  
19    // Direction vectors for moving up, right, down, left
20    const directions = [-1, 0, 1, 0, -1];
21  
22    // Dijkstra's algorithm to find shortest path
23    while (!priorityQueue.isEmpty()) {
24        const [currentTime, currentRow, currentCol] = priorityQueue.dequeue();
25      
26        // Skip if we've already found a better path to this cell
27        if (currentTime > distances[currentRow][currentCol]) {
28            continue;
29        }
30      
31        // Check if we've reached the destination
32        if (currentRow === rows - 1 && currentCol === cols - 1) {
33            return currentTime;
34        }
35      
36        // Explore all four adjacent cells
37        for (let dirIndex = 0; dirIndex < 4; dirIndex++) {
38            const nextRow = currentRow + directions[dirIndex];
39            const nextCol = currentCol + directions[dirIndex + 1];
40          
41            // Check if the next cell is within grid bounds
42            if (nextRow >= 0 && nextRow < rows && nextCol >= 0 && nextCol < cols) {
43                // Calculate time to move to next cell:
44                // Must wait until moveTime[nextRow][nextCol] if it's greater than current time,
45                // then add 1 for the move itself
46                const timeToNextCell = Math.max(moveTime[nextRow][nextCol], currentTime) + 1;
47              
48                // Update if we found a shorter path to the next cell
49                if (timeToNextCell < distances[nextRow][nextCol]) {
50                    distances[nextRow][nextCol] = timeToNextCell;
51                    priorityQueue.enqueue([timeToNextCell, nextRow, nextCol]);
52                }
53            }
54        }
55    }
56  
57    // Return -1 if destination is unreachable (though this shouldn't happen in valid input)
58    return -1;
59}
60

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 cell (i, j) in the n × m grid can be visited and added to the priority queue
  • The priority queue can contain at most n × m elements
  • Each heap operation (push and pop) takes O(log(n × m)) time
  • We perform at most n × m pop operations (one for each cell)
  • For each popped cell, we check up to 4 neighbors, potentially adding them to the queue
  • This results in at most O(n × m) heap push operations

Therefore, the total 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: at most O(n × m) elements in the worst case
  • Other variables like dirs and loop variables: O(1)

The dominant space usage comes from the dist matrix and the priority queue, both of which are O(n × m), resulting in a total space complexity of O(n × m).

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

Common Pitfalls

1. Incorrect Time Calculation - Forgetting the Waiting Time

The Pitfall: A common mistake is to calculate the arrival time as simply dist[i][j] + 1 without considering that you might need to wait for the room to open. This would lead to entering rooms before they're accessible.

Incorrect Implementation:

# WRONG: This doesn't account for waiting
next_time = dist[current_row][current_col] + 1

Correct Implementation:

# CORRECT: Take the maximum of current position time and room opening time
next_time = max(moveTime[next_row][next_col], dist[current_row][current_col]) + 1

2. Off-by-One Error in Time Calculation

The Pitfall: Another frequent error is incorrectly handling when exactly you can enter a room. Some might think if you're at room (i,j) at time t and the next room opens at time t, you can enter immediately. However, moving takes 1 second, so you arrive at time t+1.

Incorrect Implementation:

# WRONG: This allows entering at the exact opening time without movement cost
if dist[current_row][current_col] >= moveTime[next_row][next_col]:
    next_time = dist[current_row][current_col] + 1
else:
    next_time = moveTime[next_row][next_col]  # Missing the +1 for movement!

Correct Implementation:

# CORRECT: Always add 1 for the movement after considering the waiting time
next_time = max(moveTime[next_row][next_col], dist[current_row][current_col]) + 1

3. Using BFS Instead of Dijkstra's Algorithm

The Pitfall: Since this looks like a shortest path problem on a grid, it's tempting to use BFS. However, BFS assumes uniform edge weights, but here the "cost" to reach each room varies based on opening times, making rooms have different effective distances.

Why BFS Fails:

  • BFS explores cells level by level, assuming each move has equal cost
  • In this problem, waiting times create variable costs between cells
  • A path with more moves might actually be faster if it avoids rooms with late opening times

Example Where BFS Would Fail:

moveTime = [[0, 100],
            [1, 2]]
# BFS might find path (0,0) → (0,1) → (1,1) with time 102
# But optimal path is (0,0) → (1,0) → (1,1) with time 3

4. Not Handling the Starting Position Correctly

The Pitfall: Forgetting that you start at (0, 0) at time 0, regardless of moveTime[0][0]. The opening time of the starting room is irrelevant since you're already there.

Incorrect Implementation:

# WRONG: Considering the opening time of the starting position
dist[0][0] = moveTime[0][0]
priority_queue = [(moveTime[0][0], 0, 0)]

Correct Implementation:

# CORRECT: Start at time 0 regardless of moveTime[0][0]
dist[0][0] = 0
priority_queue = [(0, 0, 0)]
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

The three-steps of Depth First Search are:

  1. Identify states;
  2. Draw the state-space tree;
  3. DFS on the state-space tree.

Recommended Readings

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

Load More