Facebook Pixel

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.

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 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 size n x m where dist[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:

  1. Extract minimum: Pop the state with minimum time (d, i, j) from the priority queue.

  2. Check destination: If we've reached (n-1, m-1), return the time d as our answer.

  3. Skip outdated states: If the current time d is greater than dist[i][j], skip this state as we've already found a better path to this room.

  4. Explore neighbors: For each of the four adjacent rooms (x, y):

    • Check if the position is valid: 0 <= x < n and 0 <= y < m
    • Calculate the time to enter room (x, y):
      t = max(moveTime[x][y], dist[i][j]) + (i + j) % 2 + 1
      This formula accounts for:
      • Waiting time if we arrive before moveTime[x][y]
      • The alternating movement cost based on position parity
  5. 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

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 Evaluator

Example 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:

  1. Movement costs alternate based on position parity
  2. We must wait if arriving before the allowed entry time
  3. 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 most n × 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) and O(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 contain O(n × m) elements if multiple paths to cells are discovered before being processed
  • Other variables like dirs and loop variables use O(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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What are the most two important steps in writing a depth first search function? (Select 2)


Recommended Readings

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

Load More