LeetCode 3341 - Find Minimum Time to Reach Last Room I
The problem is asking for the minimum time required to reach the bottom-right room (n - 1, m - 1) in a dungeon represented as an n x m grid. Each room (i, j) has a moveTime[i][j], which specifies the earliest time at which the room can be entered.
Difficulty: 🟡 Medium
Topics: Array, Graph Theory, Heap (Priority Queue), Matrix, Shortest Path
Solution
Problem Understanding
The problem is asking for the minimum time required to reach the bottom-right room (n - 1, m - 1) in a dungeon represented as an n x m grid. Each room (i, j) has a moveTime[i][j], which specifies the earliest time at which the room can be entered. You start at (0, 0) at time t = 0 and can move to adjacent rooms (up, down, left, right), taking exactly one second per move. The challenge is to compute the minimum time to reach the final room while respecting the moveTime constraints.
The input consists of a 2D integer array, where n and m are both between 2 and 50. The move times can be large, up to 10^9, so a naive solution that simulates every second is impractical. The output is a single integer representing the earliest time you can arrive at (n - 1, m - 1).
Important considerations include handling rooms that are initially inaccessible due to their moveTime and ensuring moves are only made when a room is open. Edge cases to watch for include rooms that require waiting before entering, very small grids (2x2), and paths where detours are required to respect timing constraints.
Approaches
The brute-force approach would simulate movement second by second, checking which rooms are available at each time and exploring all possible paths. This could involve a BFS that tracks current time and available rooms. While correct, this approach can be extremely slow because the maximum moveTime can be 10^9, and the BFS would require infeasible computation and memory.
The optimal approach treats this as a shortest-path problem with time constraints, which is naturally solved using a priority queue (min-heap) combined with Dijkstra’s algorithm. Each state is defined by the position (i, j) and the earliest time you can arrive at that room. From a room (i, j) at time t_curr, moving to an adjacent room (ni, nj) will take one second, but you may have to wait if t_curr + 1 < moveTime[ni][nj]. Using a min-heap ensures we always expand the room that can be reached at the earliest time, guaranteeing correctness.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(4^(n*m)) | O(n*m) | BFS simulation of all moves by second, too slow for large moveTimes |
| Optimal | O(n_m * log(n_m)) | O(n*m) | Dijkstra-style BFS with min-heap, expands each cell at most once |
Algorithm Walkthrough
- Initialize a min-heap (priority queue) with the starting room
(0, 0)and timet = 0. - Create a 2D array
visitedorearliestTimeof sizen x mto store the minimum time at which each room is reached. Initialize all cells with infinity except(0, 0)which is 0. - Define the possible moves as
(dx, dy)pairs: right, left, down, up. - While the heap is not empty, pop the room with the smallest current time
t_curr. - For each adjacent room
(ni, nj):
a. Compute next_time = t_curr + 1 because moving takes one second.
b. If next_time < moveTime[ni][nj], set next_time = moveTime[ni][nj] to wait until the room opens.
c. If next_time is smaller than the previously recorded time for (ni, nj), update earliestTime[ni][nj] and push (next_time, ni, nj) into the heap.
6. Repeat until the heap is empty or until (n - 1, m - 1) is reached.
7. Return earliestTime[n - 1][m - 1] as the minimum time to reach the last room.
Why it works: Using a min-heap guarantees that we always expand the room reachable at the earliest time. Waiting for a room to open is explicitly handled by comparing current time and moveTime, ensuring the algorithm respects the dungeon's timing constraints. Since each cell is expanded only when a smaller arrival time is found, this avoids unnecessary revisits and guarantees the optimal solution.
Python Solution
from heapq import heappush, heappop
from typing import List
class Solution:
def minTimeToReach(self, moveTime: List[List[int]]) -> int:
n, m = len(moveTime), len(moveTime[0])
earliestTime = [[float('inf')] * m for _ in range(n)]
earliestTime[0][0] = 0
heap = [(0, 0, 0)] # (time, row, col)
directions = [(0,1),(1,0),(0,-1),(-1,0)]
while heap:
t_curr, i, j = heappop(heap)
if (i, j) == (n-1, m-1):
return t_curr
if t_curr > earliestTime[i][j]:
continue
for dx, dy in directions:
ni, nj = i + dx, j + dy
if 0 <= ni < n and 0 <= nj < m:
next_time = max(t_curr + 1, moveTime[ni][nj])
if next_time < earliestTime[ni][nj]:
earliestTime[ni][nj] = next_time
heappush(heap, (next_time, ni, nj))
return earliestTime[n-1][m-1]
Explanation: We initialize a min-heap with (0, 0, 0) representing time and coordinates. earliestTime stores the minimum time at which each cell is reached. For each popped cell, we examine all four directions, calculate the next reachable time considering potential waits, update if it is better than the previously recorded time, and push into the heap. The function returns immediately when the last room is popped from the heap, guaranteeing the earliest time.
Go Solution
package main
import (
"container/heap"
"math"
)
type State struct {
time, row, col int
}
type PriorityQueue []State
func (pq PriorityQueue) Len() int { return len(pq) }
func (pq PriorityQueue) Less(i, j int) bool { return pq[i].time < pq[j].time }
func (pq PriorityQueue) Swap(i, j int) { pq[i], pq[j] = pq[j], pq[i] }
func (pq *PriorityQueue) Push(x interface{}) { *pq = append(*pq, x.(State)) }
func (pq *PriorityQueue) Pop() interface{} {
old := *pq
n := len(old)
x := old[n-1]
*pq = old[0 : n-1]
return x
}
func minTimeToReach(moveTime [][]int) int {
n, m := len(moveTime), len(moveTime[0])
earliestTime := make([][]int, n)
for i := range earliestTime {
earliestTime[i] = make([]int, m)
for j := range earliestTime[i] {
earliestTime[i][j] = math.MaxInt64
}
}
earliestTime[0][0] = 0
pq := &PriorityQueue{}
heap.Init(pq)
heap.Push(pq, State{0, 0, 0})
directions := [][2]int{{0,1},{1,0},{0,-1},{-1,0}}
for pq.Len() > 0 {
curr := heap.Pop(pq).(State)
t_curr, i, j := curr.time, curr.row, curr.col
if i == n-1 && j == m-1 {
return t_curr
}
if t_curr > earliestTime[i][j] {
continue
}
for _, d := range directions {
ni, nj := i + d[0], j + d[1]
if ni >= 0 && ni < n && nj >= 0 && nj < m {
next_time := t_curr + 1
if next_time < moveTime[ni][nj] {
next_time = moveTime[ni][nj]
}
if next_time < earliestTime[ni][nj] {
earliestTime[ni][nj] = next_time
heap.Push(pq, State{next_time, ni, nj})
}
}
}
}
return earliestTime[n-1][m-1]
}
Explanation: The Go implementation mirrors the Python solution using a priority queue. We define a State struct for each room, implement the heap interface, and track the earliest time for each room. Go requires explicit heap initialization and interface implementation, but the core algorithm and logic are identical.
Worked Examples
Example 1: moveTime = [[0,4],[4,4]]
| Step | Heap | Current | Adjacent Updates | earliestTime |
|---|---|---|---|---|
| 1 | [(0,0,0)] | (0,0) | right:(0,1) time=max(1,4)=4, down:(1,0) time=max(1,4)=4 | [[0,4],[4,inf |