2577. Minimum Time to Visit a Cell In a Grid
Problem Description
You are given an m x n
matrix grid
consisting of non-negative integers. Each cell grid[row][col]
represents the minimum time required to visit that cell - meaning you can only visit cell (row, col)
when your current time is greater than or equal to grid[row][col]
.
You start at the top-left cell (0, 0)
at time 0
. From any cell, you can move to an adjacent cell in four directions (up, down, left, right), and each move takes exactly 1
second.
Your goal is to find the minimum time required to reach the bottom-right cell (m-1, n-1)
of the matrix. If it's impossible to reach the bottom-right cell, return -1
.
Key Points:
- You begin at position
(0, 0)
at time0
- Each move to an adjacent cell takes
1
second - You can only enter a cell if your current time ≥ the cell's value
- You need to reach position
(m-1, n-1)
in minimum time - Return
-1
if the destination is unreachable
Example of the time constraint: If you're at a cell at time 5
and want to move to an adjacent cell with value 8
, you cannot enter immediately. You would need to wait until time 8
to enter that cell.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The grid can be viewed as a graph where each cell is a node and adjacent cells are connected by edges. We need to traverse from one cell to another.
Is it a tree?
- No: The grid is not a tree structure. It's a grid graph where cells can be revisited and there can be multiple paths between cells.
Is the problem related to directed acyclic graphs?
- No: The grid allows movement in all four directions and we can revisit cells, so it's not a DAG.
Is the problem related to shortest paths?
- Yes: We need to find the minimum time (shortest path) to reach from the top-left cell to the bottom-right cell.
Is the graph weighted?
- Yes: Each cell has a time constraint (weight) that affects when we can visit it. The time required to enter a cell depends on its value in the grid.
Dijkstra's Algorithm
- Yes: Since we have a weighted graph shortest path problem, Dijkstra's algorithm is appropriate.
Conclusion: The flowchart suggests using Dijkstra's Algorithm for this problem. While the flowchart leads us to Dijkstra's (which is indeed the correct approach), it's worth noting that this is essentially a modified BFS with a priority queue. The priority queue ensures we always process cells in order of their earliest possible visit time, making it a weighted shortest path problem that BFS alone cannot handle efficiently.
Intuition
The key insight is recognizing that this is a shortest path problem with a twist - we have time constraints on when we can enter each cell. Let's think through why standard BFS won't work and how we arrive at the solution.
In a typical shortest path problem on a grid, BFS would work perfectly because each move has the same cost (1 second). However, here we have an additional constraint: we can only enter a cell when our current time is at least grid[x][y]
.
Consider what happens when we reach a cell at time t
but need to enter an adjacent cell that requires time grid[x][y] > t + 1
. We can't just move there immediately - we need to wait. But how do we wait? We can move back and forth between accessible cells to "kill time" until we reach the required timestamp.
This leads to an important observation: if we can move back and forth between two cells, we can extend our waiting time by any even number of seconds (move away and back = 2 seconds). This means:
- If we arrive at time
t
and need timegrid[x][y]
, and the difference(grid[x][y] - t)
is odd, we arrive atgrid[x][y]
- If the difference is even, we need one extra second, arriving at
grid[x][y] + 1
This waiting mechanism only works if we can actually move from the starting position. If both adjacent cells from (0,0)
require time > 1, we're stuck and should return -1
.
Since different paths might reach the same cell at different times, and we want the minimum time to reach the destination, we need to track the earliest arrival time at each cell. This naturally leads us to Dijkstra's algorithm - we use a priority queue to always process the cell we can reach earliest, ensuring we find the optimal path.
The algorithm essentially becomes: explore cells in order of their earliest reachable time, calculating the actual arrival time considering both movement cost and waiting time needed to satisfy the cell's time constraint.
Learn more about Breadth-First Search, Graph, Shortest Path and Heap (Priority Queue) patterns.
Solution Approach
Let's walk through the implementation step by step:
Initial Check for Impossibility:
First, we check if movement is possible at all. If both grid[0][1] > 1
and grid[1][0] > 1
, we cannot move from the starting position (0,0)
since we start at time 0 and need at least time 2 to enter either adjacent cell. In this case, we return -1
.
Data Structure Setup:
- Create a 2D array
dist
of sizem × n
initialized with infinity (inf
) to track the minimum time to reach each cell - Set
dist[0][0] = 0
since we start at the top-left corner at time 0 - Initialize a min-heap priority queue
q
with the starting state(0, 0, 0)
representing(time, row, col)
Dijkstra's Algorithm with Time Adjustment: We process cells in order of their earliest reachable time using the priority queue:
- Extract minimum: Pop the cell
(t, i, j)
with the smallest time from the heap - Check destination: If we've reached
(m-1, n-1)
, return the timet
- Explore neighbors: For each of the 4 adjacent cells
(x, y)
:- Calculate the base arrival time:
nt = t + 1
- Handle time constraints: If
nt < grid[x][y]
, we need to wait:- Calculate the waiting time needed:
wait = grid[x][y] - nt
- If
wait
is even, we arrive exactly atgrid[x][y]
- If
wait
is odd, we need one extra second, arriving atgrid[x][y] + 1
- This is elegantly handled by:
nt = grid[x][y] + (grid[x][y] - nt) % 2
- Calculate the waiting time needed:
- Update if better: If this new time
nt < dist[x][y]
:- Update
dist[x][y] = nt
- Push
(nt, x, y)
to the priority queue
- Update
- Calculate the base arrival time:
Direction Handling:
The code uses a clever pattern with dirs = (-1, 0, 1, 0, -1)
and pairwise(dirs)
to generate the four directions:
(-1, 0)
for up(0, 1)
for right(1, 0)
for down(0, -1)
for left
Why This Works: The priority queue ensures we always process the cell we can reach earliest. When we update a cell's distance, we're guaranteed to have found the optimal way to reach it considering both movement time and waiting constraints. The modulo operation cleverly handles the back-and-forth movement pattern needed to adjust our arrival time to match the cell's requirement.
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 this 3x3 grid:
grid = [[0, 2, 4], [3, 2, 1], [1, 0, 4]]
Initial Setup:
- Start at (0,0) at time 0
- Check if movement is possible: grid[0][1] = 2 and grid[1][0] = 3
- Since 2 > 1 and 3 > 1, we cannot move immediately from start. Return -1.
Let's modify the example to make it solvable:
grid = [[0, 1, 4], [3, 2, 1], [1, 0, 4]]
Step-by-step execution:
-
Initialize:
- dist = [[0, inf, inf], [inf, inf, inf], [inf, inf, inf]]
- Priority queue: [(0, 0, 0)]
-
Process (0, 0, 0) - time=0, position=(0,0):
- Check neighbors:
- Right (0,1): nt = 0 + 1 = 1, grid[0][1] = 1, so arrive at time 1
- Update dist[0][1] = 1, add (1, 0, 1) to queue
- Down (1,0): nt = 0 + 1 = 1, grid[1][0] = 3
- Need to wait: 3 - 1 = 2 (even difference)
- Arrive at time 3
- Update dist[1][0] = 3, add (3, 1, 0) to queue
- Right (0,1): nt = 0 + 1 = 1, grid[0][1] = 1, so arrive at time 1
- Check neighbors:
-
Process (1, 0, 1) - time=1, position=(0,1):
- Check neighbors:
- Left (0,0): Already visited with better time
- Right (0,2): nt = 1 + 1 = 2, grid[0][2] = 4
- Need to wait: 4 - 2 = 2 (even difference)
- Arrive at time 4
- Update dist[0][2] = 4, add (4, 0, 2) to queue
- Down (1,1): nt = 1 + 1 = 2, grid[1][1] = 2
- Can enter exactly at time 2
- Update dist[1][1] = 2, add (2, 1, 1) to queue
- Check neighbors:
-
Process (2, 1, 1) - time=2, position=(1,1):
- Check neighbors:
- Up (0,1): Already visited with better time
- Right (1,2): nt = 2 + 1 = 3, grid[1][2] = 1
- No wait needed (3 ≥ 1)
- Update dist[1][2] = 3, add (3, 1, 2) to queue
- Down (2,1): nt = 2 + 1 = 3, grid[2][1] = 0
- No wait needed (3 ≥ 0)
- Update dist[2][1] = 3, add (3, 2, 1) to queue
- Check neighbors:
-
Continue processing until we reach (2,2):
- From (1,2) at time 3: Can reach (2,2) at time 4 (since grid[2][2] = 4)
- From (2,1) at time 3: Can reach (2,2) at time 5 (need to wait: 4-4=0, but we arrive at 4+1=5 due to odd/even rule)
-
Result: The minimum time to reach (2,2) is 4.
Key observations from this example:
- We can't always move immediately - sometimes we need to wait
- The waiting mechanism (back-and-forth movement) adds either 0 or 1 extra second depending on parity
- Dijkstra's algorithm ensures we find the optimal path by always processing the earliest reachable cell first
- The initial impossibility check prevents wasted computation when no path exists
Solution Implementation
1from typing import List
2from heapq import heappush, heappop
3from math import inf
4
5class Solution:
6 def minimumTime(self, grid: List[List[int]]) -> int:
7 # Edge case: if both initial adjacent cells require time > 1,
8 # we can't move anywhere from start (stuck at position [0,0])
9 if grid[0][1] > 1 and grid[1][0] > 1:
10 return -1
11
12 rows, cols = len(grid), len(grid[0])
13
14 # Initialize distance matrix to track minimum time to reach each cell
15 min_time = [[inf] * cols for _ in range(rows)]
16 min_time[0][0] = 0
17
18 # Priority queue: (time, row, col)
19 # Using min-heap to always process the cell with minimum time first
20 priority_queue = [(0, 0, 0)]
21
22 # Direction vectors for moving up, right, down, left
23 directions = [(-1, 0), (0, 1), (1, 0), (0, -1)]
24
25 while priority_queue:
26 current_time, current_row, current_col = heappop(priority_queue)
27
28 # If we reached the destination, return the time
29 if current_row == rows - 1 and current_col == cols - 1:
30 return current_time
31
32 # Explore all four adjacent cells
33 for delta_row, delta_col in directions:
34 next_row = current_row + delta_row
35 next_col = current_col + delta_col
36
37 # Check if the next position is within grid bounds
38 if 0 <= next_row < rows and 0 <= next_col < cols:
39 # Base time to reach the next cell
40 next_time = current_time + 1
41
42 # If we arrive too early, we need to wait
43 # We can move back and forth to kill time
44 if next_time < grid[next_row][next_col]:
45 wait_time = grid[next_row][next_col] - next_time
46 # If wait time is odd, we arrive at exact time
47 # If wait time is even, we need one more move
48 next_time = grid[next_row][next_col] + wait_time % 2
49
50 # Only process if we found a better path to this cell
51 if next_time < min_time[next_row][next_col]:
52 min_time[next_row][next_col] = next_time
53 heappush(priority_queue, (next_time, next_row, next_col))
54
55 # Should not reach here if a valid path exists
56 return -1
57
1class Solution {
2 public int minimumTime(int[][] grid) {
3 // Edge case: If both adjacent cells from start require time > 1,
4 // we cannot move anywhere (stuck at start)
5 if (grid[0][1] > 1 && grid[1][0] > 1) {
6 return -1;
7 }
8
9 int rows = grid.length;
10 int cols = grid[0].length;
11
12 // Initialize distance array with maximum values
13 int[][] minTimeToReach = new int[rows][cols];
14 for (int[] row : minTimeToReach) {
15 Arrays.fill(row, Integer.MAX_VALUE);
16 }
17 minTimeToReach[0][0] = 0;
18
19 // Priority queue to store [time, row, col], sorted by time
20 PriorityQueue<int[]> priorityQueue = new PriorityQueue<>((a, b) -> a[0] - b[0]);
21 priorityQueue.offer(new int[] {0, 0, 0}); // [time, row, col]
22
23 // Direction vectors for moving up, right, down, left
24 int[] directions = {-1, 0, 1, 0, -1};
25
26 while (!priorityQueue.isEmpty()) {
27 int[] current = priorityQueue.poll();
28 int currentTime = current[0];
29 int currentRow = current[1];
30 int currentCol = current[2];
31
32 // Check if we reached the destination
33 if (currentRow == rows - 1 && currentCol == cols - 1) {
34 return currentTime;
35 }
36
37 // Explore all four directions
38 for (int dir = 0; dir < 4; dir++) {
39 int nextRow = currentRow + directions[dir];
40 int nextCol = currentCol + directions[dir + 1];
41
42 // Check if the next cell is within bounds
43 if (nextRow >= 0 && nextRow < rows && nextCol >= 0 && nextCol < cols) {
44 // Calculate the next possible time to reach the neighbor
45 int nextTime = currentTime + 1;
46
47 // If we arrive too early, we need to wait
48 // We can move back and forth to kill time, so we adjust by even/odd parity
49 if (nextTime < grid[nextRow][nextCol]) {
50 int waitTime = grid[nextRow][nextCol] - nextTime;
51 // Ensure correct parity (can only arrive at even or odd times)
52 nextTime = grid[nextRow][nextCol] + waitTime % 2;
53 }
54
55 // Update if we found a better path
56 if (nextTime < minTimeToReach[nextRow][nextCol]) {
57 minTimeToReach[nextRow][nextCol] = nextTime;
58 priorityQueue.offer(new int[] {nextTime, nextRow, nextCol});
59 }
60 }
61 }
62 }
63
64 // This line should never be reached due to the while(true) nature of original code
65 return -1;
66 }
67}
68
1class Solution {
2public:
3 int minimumTime(vector<vector<int>>& grid) {
4 // Special case: if both adjacent cells from start require time > 1,
5 // we cannot move anywhere (stuck at start)
6 if (grid[0][1] > 1 && grid[1][0] > 1) {
7 return -1;
8 }
9
10 int rows = grid.size();
11 int cols = grid[0].size();
12
13 // Distance array to track minimum time to reach each cell
14 int distance[rows][cols];
15 memset(distance, 0x3f, sizeof(distance)); // Initialize with large value
16 distance[0][0] = 0;
17
18 // Min heap: stores (time, row, col)
19 using Tuple = tuple<int, int, int>;
20 priority_queue<Tuple, vector<Tuple>, greater<Tuple>> minHeap;
21 minHeap.emplace(0, 0, 0);
22
23 // Direction vectors for moving up, right, down, left
24 int directions[5] = {-1, 0, 1, 0, -1};
25
26 while (true) {
27 // Get cell with minimum time
28 auto [currentTime, row, col] = minHeap.top();
29 minHeap.pop();
30
31 // Check if we reached the destination
32 if (row == rows - 1 && col == cols - 1) {
33 return currentTime;
34 }
35
36 // Explore all 4 adjacent cells
37 for (int k = 0; k < 4; ++k) {
38 int nextRow = row + directions[k];
39 int nextCol = col + directions[k + 1];
40
41 // Check if next position is within bounds
42 if (nextRow >= 0 && nextRow < rows && nextCol >= 0 && nextCol < cols) {
43 // Base time to reach next cell
44 int nextTime = currentTime + 1;
45
46 // If we arrive too early, we need to wait
47 // We can move back and forth to waste time
48 if (nextTime < grid[nextRow][nextCol]) {
49 int waitTime = grid[nextRow][nextCol] - nextTime;
50 // If wait time is odd, we need one extra move to sync parity
51 nextTime = grid[nextRow][nextCol] + waitTime % 2;
52 }
53
54 // Update if we found a better path
55 if (nextTime < distance[nextRow][nextCol]) {
56 distance[nextRow][nextCol] = nextTime;
57 minHeap.emplace(nextTime, nextRow, nextCol);
58 }
59 }
60 }
61 }
62 }
63};
64
1/**
2 * Finds the minimum time to reach from top-left to bottom-right corner of the grid
3 * @param grid - 2D array where grid[i][j] represents the earliest time to visit cell (i,j)
4 * @returns Minimum time to reach destination, or -1 if impossible
5 */
6function minimumTime(grid: number[][]): number {
7 // Early termination: if both adjacent cells from start require time > 1, we're stuck
8 if (grid[0][1] > 1 && grid[1][0] > 1) {
9 return -1;
10 }
11
12 // Grid dimensions
13 const rows: number = grid.length;
14 const cols: number = grid[0].length;
15
16 // Direction vectors for moving up, right, down, left
17 const DIRECTIONS: number[] = [-1, 0, 1, 0, -1];
18
19 // Priority queue to process cells by minimum time (Dijkstra's algorithm)
20 // Element format: [time, row, col]
21 const priorityQueue = new MinPriorityQueue({
22 priority: ([time]: number[]) => time
23 });
24
25 // Distance matrix to track minimum time to reach each cell
26 const minTimeToReach: number[][] = Array.from(
27 { length: rows },
28 () => new Array(cols).fill(Number.POSITIVE_INFINITY)
29 );
30
31 // Initialize starting position
32 minTimeToReach[0][0] = 0;
33 priorityQueue.enqueue([0, 0, 0]); // [time, row, col]
34
35 // Process cells until we reach the destination
36 while (true) {
37 // Get the cell with minimum time
38 const [currentTime, currentRow, currentCol] = priorityQueue.dequeue().element;
39
40 // Check if we've reached the destination
41 if (currentRow === rows - 1 && currentCol === cols - 1) {
42 return currentTime;
43 }
44
45 // Explore all four neighboring cells
46 for (let direction = 0; direction < 4; direction++) {
47 const nextRow: number = currentRow + DIRECTIONS[direction];
48 const nextCol: number = currentCol + DIRECTIONS[direction + 1];
49
50 // Skip if out of bounds
51 if (nextRow < 0 || nextRow >= rows || nextCol < 0 || nextCol >= cols) {
52 continue;
53 }
54
55 // Calculate the next possible time to reach the neighbor
56 let nextTime: number = currentTime + 1;
57
58 // If we arrive too early, we need to wait
59 // We can move back and forth to pass time (hence the parity check)
60 if (nextTime < grid[nextRow][nextCol]) {
61 const waitTime: number = grid[nextRow][nextCol] - nextTime;
62 // Add wait time, ensuring correct parity (we can only arrive at odd/even times)
63 nextTime = grid[nextRow][nextCol] + (waitTime % 2);
64 }
65
66 // Update if we found a better path to this cell
67 if (nextTime < minTimeToReach[nextRow][nextCol]) {
68 minTimeToReach[nextRow][nextCol] = nextTime;
69 priorityQueue.enqueue([nextTime, nextRow, nextCol]);
70 }
71 }
72 }
73}
74
Time and Space Complexity
Time Complexity: O(m × n × log(m × n))
The algorithm uses Dijkstra's algorithm with a min-heap to find the shortest path. In the worst case:
- Each cell
(i, j)
in them × n
grid can be visited and added to the heap - The heap can contain at most
m × n
elements - Each heap operation (push and pop) takes
O(log(m × n))
time - We perform at most
O(m × n)
pop operations (one for each cell) - For each popped cell, we explore up to 4 neighbors, potentially pushing them to the heap
- Total heap operations:
O(m × n)
pops andO(m × n)
pushes - Therefore, the overall time complexity is
O(m × n × log(m × n))
Space Complexity: O(m × n)
The space is used for:
- The
dist
matrix:O(m × n)
to store the minimum time to reach each cell - The priority queue
q
: In the worst case, it can containO(m × n)
elements - Other variables use constant space
- Therefore, the total space complexity is
O(m × n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Handling of the Waiting Time Calculation
One of the most common mistakes is incorrectly calculating the arrival time when we need to wait before entering a cell. Many developers initially write something like:
# INCORRECT approach if next_time < grid[next_row][next_col]: next_time = grid[next_row][next_col] # Just wait until we can enter
Why this fails: This doesn't account for the parity constraint. When we arrive too early at a cell, we can't just stand still and wait. We must keep moving (back and forth between cells) until we can enter. This back-and-forth movement means we can only arrive at times with specific parity.
The correct solution: We need to consider whether the waiting time is odd or even:
if next_time < grid[next_row][next_col]: wait_time = grid[next_row][next_col] - next_time next_time = grid[next_row][next_col] + wait_time % 2
This ensures that if we need to wait an even number of seconds, we add 1 extra second (since moving back and forth takes 2 seconds per cycle).
2. Missing the Initial Impossibility Check
Another critical pitfall is forgetting to check if movement is possible at all from the starting position:
# MISSING this crucial check if grid[0][1] > 1 and grid[1][0] > 1: return -1
Why this matters: If both adjacent cells from (0,0) require time > 1 to enter, and we start at time 0, we're stuck. We can't move anywhere to "kill time" because there's nowhere to go. Without this check, the algorithm might run indefinitely or return an incorrect result.
3. Using BFS Instead of Dijkstra's Algorithm
Some might attempt to solve this with standard BFS:
# INCORRECT: Using regular BFS
from collections import deque
queue = deque([(0, 0, 0)]) # time, row, col
visited = set()
Why BFS fails: BFS assumes all edges have equal weight, but in this problem, the actual time to move between cells varies based on the waiting requirement. We need Dijkstra's algorithm (using a priority queue) to always process the cell we can reach earliest, not just the cell that's fewest moves away.
4. Not Updating Distance Only When Better
A subtle bug can occur if we don't properly check whether we've found a better path:
# INCORRECT: Missing the optimization check heappush(priority_queue, (next_time, next_row, next_col)) min_time[next_row][next_col] = next_time
The correct approach:
if next_time < min_time[next_row][next_col]: min_time[next_row][next_col] = next_time heappush(priority_queue, (next_time, next_row, next_col))
Without this check, we might process the same cell multiple times with suboptimal paths, leading to incorrect results or performance issues.
A person thinks of a number between 1 and 1000. You may ask any number questions to them, provided that the question can be answered with either "yes" or "no".
What is the minimum number of questions you needed to ask so that you are guaranteed to know the number that the person is thinking?
Recommended Readings
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
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
Want a Structured Path to Master System Design Too? Don’t Miss This!