Facebook Pixel

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 time 0
  • 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.

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

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 time grid[x][y], and the difference (grid[x][y] - t) is odd, we arrive at grid[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 size m × 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:

  1. Extract minimum: Pop the cell (t, i, j) with the smallest time from the heap
  2. Check destination: If we've reached (m-1, n-1), return the time t
  3. 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 at grid[x][y]
      • If wait is odd, we need one extra second, arriving at grid[x][y] + 1
      • This is elegantly handled by: nt = grid[x][y] + (grid[x][y] - nt) % 2
    • Update if better: If this new time nt < dist[x][y]:
      • Update dist[x][y] = nt
      • Push (nt, x, y) to the priority queue

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 Evaluator

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

  1. Initialize:

    • dist = [[0, inf, inf], [inf, inf, inf], [inf, inf, inf]]
    • Priority queue: [(0, 0, 0)]
  2. 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
  3. 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
  4. 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
  5. 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)
  6. 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 the m × 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 and O(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 contain O(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.

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

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

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

Load More