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

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

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.

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

What does the following code do?

1def f(arr1, arr2): 2 i, j = 0, 0 3 new_arr = [] 4 while i < len(arr1) and j < len(arr2): 5 if arr1[i] < arr2[j]: 6 new_arr.append(arr1[i]) 7 i += 1 8 else: 9 new_arr.append(arr2[j]) 10 j += 1 11 new_arr.extend(arr1[i:]) 12 new_arr.extend(arr2[j:]) 13 return new_arr 14
1public static List<Integer> f(int[] arr1, int[] arr2) { 2 int i = 0, j = 0; 3 List<Integer> newArr = new ArrayList<>(); 4 5 while (i < arr1.length && j < arr2.length) { 6 if (arr1[i] < arr2[j]) { 7 newArr.add(arr1[i]); 8 i++; 9 } else { 10 newArr.add(arr2[j]); 11 j++; 12 } 13 } 14 15 while (i < arr1.length) { 16 newArr.add(arr1[i]); 17 i++; 18 } 19 20 while (j < arr2.length) { 21 newArr.add(arr2[j]); 22 j++; 23 } 24 25 return newArr; 26} 27
1function f(arr1, arr2) { 2 let i = 0, j = 0; 3 let newArr = []; 4 5 while (i < arr1.length && j < arr2.length) { 6 if (arr1[i] < arr2[j]) { 7 newArr.push(arr1[i]); 8 i++; 9 } else { 10 newArr.push(arr2[j]); 11 j++; 12 } 13 } 14 15 while (i < arr1.length) { 16 newArr.push(arr1[i]); 17 i++; 18 } 19 20 while (j < arr2.length) { 21 newArr.push(arr2[j]); 22 j++; 23 } 24 25 return newArr; 26} 27

Recommended Readings

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

Load More