Facebook Pixel

3341. Find Minimum Time to Reach Last Room I


Problem Description

You are in a dungeon structured as a grid consisting of n rows and m columns. You must navigate through this grid to reach your destination. The grid is represented by a 2D array moveTime, where each cell moveTime[i][j] specifies the earliest possible time in seconds that you can start moving into that room (i, j). You begin your journey from the top-left corner of the grid, room (0, 0), at time t = 0. Each move from one room to an adjacent room (horizontally or vertically) takes exactly one second. Your objective is to find the minimum time necessary to reach the bottom-right corner of the grid, room (n - 1, m - 1).

Intuition

To solve this problem, we can utilize Dijkstra's algorithm, which is well-suited for finding the shortest path in graphs with non-negative weights. Here, the grid can be visualized as a weighted graph where each cell is a node and each move from one cell to an adjacent cell represents an edge with a weight of one second.

  1. Set Up the Distance Array: Initialize a 2D array dist to keep track of the minimum time to reach each room. Set the initial room’s (0, 0) time to 0 and all others to infinity (inf), as we're interested in the shortest time.

  2. Priority Queue for Exploration: Use a priority queue to explore the rooms. We start by pushing the starting room (0, 0) with a time of 0 into the priority queue.

  3. Explore Using Dijkstra's Approach: With the priority queue, iteratively expand the least-time path available. For the current room (i, j), look at its adjacent rooms. Calculate the time it would take to get to an adjacent room, which is the maximum of the moveTime requirement and the accumulated time so far plus one second for the move.

  4. Update Times and Continue: If moving to the adjacent room (x, y) can be done in a time earlier than what’s currently recorded in dist[x][y], update it and push this new state into the priority queue.

  5. Terminate on Goal Room: When reaching the target room (n-1, m-1) with the least time, return this time as it's the shortest time needed given the constraints of the moveTime grid.

This intuitive approach leverages both the spatial nature of grid movement and the temporal constraints provided, efficiently finding the optimal route in terms of time.

Learn more about Graph, Shortest Path and Heap (Priority Queue) patterns.

Solution Approach

The problem of finding the minimum time to navigate the dungeon grid while adhering to the constraints of the moveTime array can be efficiently addressed using Dijkstra's Algorithm. This algorithm is effective in determining the shortest path in a graph, especially when dealing with non-negative edge weights, as is the case here where each move takes one second.

Step-by-Step Implementation:

  1. Initialize the Distance Array:

    • Create a 2D array dist of the same dimensions as moveTime which will hold the minimum time required to reach each room. Initially, set every room's time to infinity (inf), except for the starting room (0, 0), which is set to 0 since you start there at time 0.
    dist = [[inf] * m for _ in range(n)]
    dist[0][0] = 0
  2. Set Up the Priority Queue:

    • Use a priority queue to facilitate the exploration of the least-time path first. This priority queue will store tuples in the format (d, i, j), where d is the time to reach the room (i, j). Begin by inserting the starting room with its initial time into the queue.
    pq = [(0, 0, 0)]
  3. Dijkstra's Exploration:

    • While the priority queue is not empty, extract the room (i, j) with the current minimum time d.
    • If this room is the destination (n - 1, m - 1), return d as the shortest time.
    • Skip this room if the current time d exceeds dist[i][j], as this indicates a previously discovered shorter path.
    • For each of the four possible adjacent rooms (up, down, left, right), calculate the potential time t to move there, which is the maximum of the move's time constraint and the accumulated time plus one second.
  4. Update and Push New States:

    • If the new calculated time t to an adjacent room (x, y) is less than the currently recorded time in dist[x][y], update dist[x][y] and push the new state (t, x, y) into the priority queue for further exploration.
    for a, b in pairwise(dirs):
        x, y = i + a, j + b
        if 0 <= x < n and 0 <= y < m:
            t = max(moveTime[x][y], dist[i][j]) + 1
            if dist[x][y] > t:
                dist[x][y] = t
                heappush(pq, (t, x, y))
  5. Termination:

    • The algorithm naturally terminates as soon as the destination is dequeued from the priority queue with the least possible time path due to Dijkstra's greedy nature, ensuring optimality.

This structured approach efficiently navigates the grid using the properties of the priority queue to always process the most promising path next, thus ensuring that the solution is optimal in terms of the time constraints imposed.

Ready to land your dream job?

Unlock your dream job with a 2-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's illustrate the solution with a small example. Assume the moveTime grid is as follows:

moveTime = [
    [0, 2, 2],
    [2, 3, 5],
    [5, 5, 8]
]

This grid has 3 rows and 3 columns. We need to determine the minimum time needed to move from the top-left corner (0, 0) to the bottom-right corner (2, 2).

Step-by-Step Walkthrough:

  1. Initialize the Distance Array:

    We initialize dist as:

    dist = [
        [0, inf, inf],
        [inf, inf, inf],
        [inf, inf, inf]
    ]

    The starting room (0, 0) is set to 0.

  2. Set Up the Priority Queue:

    Start with a priority queue containing the starting point:

    pq = [(0, 0, 0)]  # (time, row, col)
  3. Dijkstra's Exploration:

    Process rooms by expanding from the least-time path:

    • Step 1: Dequeue (0, 0, 0) from the queue.

      • Explore adjacent rooms (0, 1) and (1, 0).
      • To (0, 1): Time = max(2, 0) + 1 = 3. Update dist[0][1] = 3 and add (3, 0, 1) to the queue.
      • To (1, 0): Time = max(2, 0) + 1 = 3. Update dist[1][0] = 3 and add (3, 1, 0) to the queue.
    • Step 2: Dequeue (3, 0, 1) (because 3 is the smallest).

      • Explore (0, 2) and (1, 1).
      • To (0, 2): Time = max(2, 3) + 1 = 4. Update dist[0][2] = 4 and add (4, 0, 2) to the queue.
      • To (1, 1): Time = max(3, 3) + 1 = 4. Update dist[1][1] = 4 and add (4, 1, 1) to the queue.
    • Step 3: Dequeue (3, 1, 0) (next smallest time).

      • Explore (2, 0) and (1, 1).
      • To (2, 0): Time = max(5, 3) + 1 = 6. Update dist[2][0] = 6 and add (6, 2, 0) to the queue.
    • Step 4: Dequeue (4, 0, 2).

      • Explore (1, 2).
      • To (1, 2): Time = max(5, 4) + 1 = 6. Update dist[1][2] = 6 and add (6, 1, 2) to the queue.
  4. Continue Exploration:

    • Follow a similar process until you reach (2, 2):
      • From (4, 1, 1), explore to (2, 1), and so on.
      • Eventually, reach (2, 2) with time 7.
  5. Termination:

    When (2, 2) is reached, the queue provides the minimum time 8. Thus, the minimum time required to navigate from (0, 0) to (2, 2) respecting the moveTime constraints is 8.

This walkthrough showcases the step-by-step application of Dijkstra's algorithm in traversing the grid based on the constraints provided by moveTime.

Solution Implementation

1from typing import List
2from heapq import heappop, heappush
3from itertools import pairwise
4from math import inf
5
6class Solution:
7    def minTimeToReach(self, moveTime: List[List[int]]) -> int:
8        n, m = len(moveTime), len(moveTime[0])  # Get the dimensions of the grid
9      
10        # Initialize distance grid with infinity
11        dist = [[inf] * m for _ in range(n)]
12        dist[0][0] = 0  # Starting point has 0 distance
13      
14        # Priority queue for Dijkstra's algorithm starting at top-left corner
15        pq = [(0, 0, 0)]
16      
17        # Directions for moving up, down, left, and right
18        dirs = (-1, 0, 1, 0, -1)
19      
20        while True:
21            d, i, j = heappop(pq)  # Pop the smallest distance node
22          
23            # If we've reached the bottom-right corner, return the total distance
24            if i == n - 1 and j == m - 1:
25                return d
26          
27            # Skip if we've found a shorter path to (i, j) already
28            if d > dist[i][j]:
29                continue
30          
31            # Explore all 4 possible directions
32            for a, b in pairwise(dirs):
33                x, y = i + a, j + b
34                # Check if the new position is within bounds
35                if 0 <= x < n and 0 <= y < m:
36                    # Calculate the time to reach this new position
37                    t = max(moveTime[x][y], dist[i][j]) + 1
38                    # If found a shorter path to (x, y), update distance and push to queue
39                    if dist[x][y] > t:
40                        dist[x][y] = t
41                        heappush(pq, (t, x, y))
42
1import java.util.Arrays;
2import java.util.PriorityQueue;
3
4class Solution {
5    public int minTimeToReach(int[][] moveTime) {
6        // Get the number of rows (n) and columns (m) from the moveTime matrix
7        int n = moveTime.length;
8        int m = moveTime[0].length;
9
10        // Initialize a distance matrix filled with maximum integer values
11        int[][] distance = new int[n][m];
12        for (int[] row : distance) {
13            Arrays.fill(row, Integer.MAX_VALUE);
14        }
15
16        // Starting point (0, 0) has a distance of 0
17        distance[0][0] = 0;
18      
19        // Priority queue to store cells in the form [current_distance, row, column]
20        // Sorted by current_distance
21        PriorityQueue<int[]> priorityQueue = new PriorityQueue<>((a, b) -> a[0] - b[0]);
22        priorityQueue.offer(new int[] {0, 0, 0});
23
24        // Directions arrays for moving up, down, left, and right
25        int[] directions = {-1, 0, 1, 0, -1};
26
27        // Process until we find the minimum distance to the bottom-right corner
28        while (true) {
29            // Get the element with the smallest distance
30            int[] current = priorityQueue.poll();
31            int currentDistance = current[0];
32            int i = current[1];
33            int j = current[2];
34
35            // Check if we've reached the bottom-right corner
36            if (i == n - 1 && j == m - 1) {
37                return currentDistance;
38            }
39
40            // If there is already a better distance, continue
41            if (currentDistance > distance[i][j]) {
42                continue;
43            }
44
45            // Explore all four possible directions (up, down, left, right)
46            for (int k = 0; k < 4; k++) {
47                int x = i + directions[k];
48                int y = j + directions[k + 1];
49              
50                // Check if new position is within bounds
51                if (x >= 0 && x < n && y >= 0 && y < m) {
52                    // Calculate new time distance for this position
53                    int newTime = Math.max(moveTime[x][y], distance[i][j]) + 1;
54
55                    // If this path offers a shorter distance, update and enqueue
56                    if (distance[x][y] > newTime) {
57                        distance[x][y] = newTime;
58                        priorityQueue.offer(new int[] {newTime, x, y});
59                    }
60                }
61            }
62        }
63    }
64}
65
1#include <vector>
2#include <queue>
3#include <array>
4#include <climits>
5
6class Solution {
7public:
8    int minTimeToReach(std::vector<std::vector<int>>& move_time) {
9        int n = move_time.size(); // Number of rows
10        int m = move_time[0].size(); // Number of columns
11
12        // Distance matrix to store minimum time to reach each cell
13        std::vector<std::vector<int>> dist(n, std::vector<int>(m, INT_MAX));
14        dist[0][0] = 0; // Starting point with 0 time
15
16        // Priority queue to process cells in increasing order of time
17        std::priority_queue<std::array<int, 3>, std::vector<std::array<int, 3>>, std::greater<>> pq;
18        pq.push({0, 0, 0}); // Push starting cell with time 0
19
20        int directions[5] = {-1, 0, 1, 0, -1}; // Movement directions for up, down, left, and right
21
22        while (!pq.empty()) {
23            auto [d, i, j] = pq.top(); // Get the cell with the smallest time
24            pq.pop();
25
26            // If we have reached the bottom-right corner, return the time
27            if (i == n - 1 && j == m - 1) {
28                return d;
29            }
30          
31            // If this path doesn't improve the time for the cell, skip it
32            if (d > dist[i][j]) {
33                continue;
34            }
35
36            // Check all 4 possible directions
37            for (int dir = 0; dir < 4; ++dir) {
38                int x = i + directions[dir];
39                int y = j + directions[dir + 1];
40
41                // If the neighboring cell is within bounds
42                if (x >= 0 && x < n && y >= 0 && y < m) {
43                    // Calculate the time to move to the neighbor cell
44                    int new_time = std::max(move_time[x][y], dist[i][j]) + 1;
45                  
46                    // If the new time is better, update the distance and push to queue
47                    if (dist[x][y] > new_time) {
48                        dist[x][y] = new_time;
49                        pq.push({new_time, x, y});
50                    }
51                }
52            }
53        }
54        // This point should not be reached due to the condition of reaching the destination
55        return -1; // Returning -1 to indicate an unexpected case
56    }
57};
58
1// Define the main function to calculate the minimum time to reach the bottom-right corner of the grid
2function minTimeToReach(moveTime: number[][]): number {
3    const [n, m] = [moveTime.length, moveTime[0].length]; // Get dimensions of the grid
4    const dist: number[][] = Array.from({ length: n }, () => Array(m).fill(Infinity)); // Create a distance matrix initialized to Infinity
5    dist[0][0] = 0; // Start at the top-left corner with a distance of 0
6
7    // Priority queue to keep track of cells to visit, sorted by the distance
8    const pq = new PriorityQueue({ compare: (a, b) => a[0] - b[0] });
9    pq.enqueue([0, 0, 0]); // Enqueue the starting position with a distance of 0
10
11    // Directions for moving in the grid (up, right, down, left)
12    const dirs = [-1, 0, 1, 0, -1];
13
14    // Loop until the priority queue is empty or the bottom-right corner is reached
15    while (true) {
16        const [d, i, j] = pq.dequeue(); // Dequeue the cell with the smallest distance
17      
18        // Check if we have reached the bottom-right corner
19        if (i === n - 1 && j === m - 1) {
20            return d; // Return the distance for reaching the bottom-right corner
21        }
22      
23        // Skip this position if a shorter path has been found
24        if (d > dist[i][j]) {
25            continue; // Continue with the next iteration if the current distance is already better
26        }
27      
28        // Explore adjacent cells
29        for (let k = 0; k < 4; ++k) {
30            const [x, y] = [i + dirs[k], j + dirs[k + 1]]; // Calculate new cell coordinates
31            // Check if the new coordinates are within the grid boundaries
32            if (x >= 0 && x < n && y >= 0 && y < m) {
33                // Calculate the new distance considering the current time
34                const t = Math.max(moveTime[x][y], dist[i][j]) + 1;
35                // If a shorter distance is found, update and enqueue the new position
36                if (dist[x][y] > t) {
37                    dist[x][y] = t; // Update distance matrix
38                    pq.enqueue([t, x, y]); // Enqueue the new position with updated distance
39                }
40            }
41        }
42    } 
43}
44

Time and Space Complexity

The time complexity of the code is O(n × m × log(n × m)). This arises from the fact that the algorithm uses Dijkstra's algorithm with a priority queue (min-heap). Pushing and popping from the heap costs O(log k) where k is the number of elements in the heap, and k can reach up to n × m in the worst case, where n is the number of rows and m is the number of columns. For each cell, the algorithm considers up to four neighboring cells, leading to O(n × m × log(n × m)) in total.

The space complexity of the code is O(n × m). This is due to the storage of the dist array that keeps track of the minimum time to reach each cell, and the priority queue pq that can store up to n × m elements.

Learn more about how to find time and space complexity quickly.


Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:
Question 1 out of 10

Which of the two traversal algorithms (BFS and DFS) can be used to find whether two nodes are connected?


Recommended Readings

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


Load More