2814. Minimum Time Takes to Reach Destination Without Drowning


Problem Description

In this problem, you are given a grid represented by strings. The grid is 0-indexed and consists of various cells denoted by characters:

  • "S": indicating your starting position.
  • "D": indicating your destination.
  • ".": representing empty cells that you can move through.
  • "X": representing stone cells that you can't move through.
  • "*": representing flooded cells that you can't move through or into in the same second they become flooded.

Your task is to move from the "S" cell to the "D" cell in the minimum number of seconds, but there's a catch. With each second that passes, any empty cell (".") adjacent to a flooded cell ("*") will also become flooded. You must take this into account because you cannot step onto a cell that is about to be flooded at the same time you step onto it. You are able to move to any adjacent cell that shares a side with your current cell.

The goal is to calculate the minimum number of seconds required to reach the destination "D" without being drowned by the flooding cells and without stepping onto the stone cells "X". The function should return this minimum number of seconds if it's possible to reach the destination or -1 if it's not possible. The destination cell will never become flooded.

Intuition

The solution is based on a breadth-first search (BFS) algorithm. The idea is to explore the grid level by level, starting from the initially flooded cells "*" and marking the timing when each cell becomes flooded. This way, it's possible to understand at each second which cells are safe to step on.

Firstly, we perform a BFS starting from the initially flooded cells to determine the exact second when each cell will become flooded. This step marks an important constraint which later helps to ensure that we don't step into a cell in the same second it's getting flooded.

Secondly, after knowing the timings, another BFS is initiated from the starting cell to navigate through the grid and search for the destination cell. During this BFS, we ensure not to step into cells that are stones "X", already visited, or about to be flooded. By comparing the current time with the flooding time of a cell, we ensure a safe path to the destination. If we manage to reach the "D" cell, the current time would represent the minimum seconds needed to get there.

The solution keeps track of visited cells to avoid revisiting them and potentially entering an infinite loop. After searching all possible paths, if the destination is reached, the time it took to get there is returned; otherwise, the function returns -1 to indicate the destination is unreachable due to the constraints provided by the stones and flooding.

Learn more about Breadth-First Search patterns.

Not Sure What to Study? Take the 2-min Quiz to Find Your Missing Piece:

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?

Solution Approach

The implementation of the solution uses a Breadth-First Search (BFS) algorithm, executed twice. BFS is ideal for this type of problem because it explores the neighbors of a cell, then moves on to their neighbors in waves, perfectly capturing the spreading nature of the flood every second.

The first part of the solution determines when each cell becomes flooded. It initializes a grid g to keep track of the flood time using inf to represent cells that are not yet known to be flooded. Another grid vis (short for visited) is used to keep track of the cells that have already been explored during this process to avoid redundantly processing them. A queue q is used to manage the order in which cells are explored.

We begin by identifying all initially flooded cells ("*") and the starting position ("S"), then perform the BFS from the flooded cells. At each wave (or level) of the BFS, for each cell being explored, we mark the cells that could be flooded next and update their flood time in g. We use dirs to access north, south, east, and west neighbors.

After we finish the first BFS, we have information about when each cell becomes flooded and can safely navigate the grid without getting drowned.

The second part uses another BFS to navigate from the "S" to "D" by exploring available paths. It uses the same vis grid (reset to False everywhere) to keep track of cells that have been visited in this phase. As we proceed, we compare the current time with the flooding time of each cell from g. If it's safe to move to the next cell (i.e., the flood time is greater than t + 1, ensuring that we won't move into an about-to-be-flooded cell), we update the queue with new cells to visit. If we find the "D" cell, we return the current time as the minimum number of seconds required to reach it.

If we complete the BFS search without finding the "D" cell, the function returns -1 to indicate that the path is impossible due to the constraints of the grid.

The solution uses a layered approach to BFS, where the first layer is checking the flooding, and the second one is navigating through the grid. This ensures that the solution accounts for the dynamic nature of the problem where the environment (the land) changes over time.

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

Which algorithm should you use to find a node that is close to the root of the tree?

Example Walkthrough

Let's illustrate the solution approach with a small example grid:

1S..
2.*.
3..D

Here, "S" is our starting position, "." represents open cells, "*" represents an initially flooded cell, and "D" is our destination.

  1. Initialization of Flood Time and Visited Grids: Our grid g to track flooding times would initially be set with inf, and vis grid is all False.
1g = [[inf, inf, inf],
2     [inf, 0,   inf],
3     [inf, inf, inf]]
4
5vis = [[False, False, False],
6       [False, True,  False],
7       [False, False, False]]

We set the position with "*" to 0 on the flood grid, and True on the visited grid as it's already flooded and has been accounted for.

  1. First BFS (Flood Spread):
    • Start with the positions marked with "*"; in this case, it's the cell at (1, 1). Queue q contains this initial cell: [(1, 1)].
    • For each cell (i, j) we process, we look at its direct neighbors to the north, south, east, and west, which aren't blocked by "X" or already visited.
    • For each neighboring cell (ni, nj), we set its flood time in g to be one more than the current cell and mark it as visited, then add it to the queue.

As a result, after spreading, the grid will have the following flooding times:

1g = [[1, 2, inf],
2     [1, 0,   1],
3     [inf, 2, inf]]
  1. Second BFS (Finding Path):
    • Reset our vis grid to all False and place the starting position (0, 0) onto our queue q: [(0, 0)].
    • Perform a BFS from that position, only moving to cells that are not flooded ("X"), not about to be flooded (according to our g grid and time t), and not already visited in this phase of the BFS.
    • As we continue this process, whenever we move to a new cell, we check its value. If it's "D", we've found our destination and return the current second as the answer. Otherwise, we add all the safe new cells we can move to into our queue.

In our example, the second BFS would process the cells like this:

  • From (0, 0), we can move to (0, 1) because it won't be flooded until time 2. We cannot move directly to the right because that cell will be flooded at the next time step. Our path and time-tracking are as follows:
1Path = S->(0, 1)
2Time = 1
  • Then from (0, 1), we can move to (1, 2) which is safe since it will be flooded at time 1, and by the time we want to step there, it will be time 2.
1Path = S->(0, 1)->(1, 2)
2Time = 2
  • Finally, from (1, 2), we can move to (2, 2) where the "D" is, as it is an open cell that will not be flooded.
1Path = S->(0, 1)->(1, 2)->(2, 2)
2Time = 3

So, the minimum time to get from "S" to "D" without drowning is 3 seconds given this particular grid. If at any point we couldn't move to a new cell because all options were blocked, visited, or about to be flooded, we would have returned -1.

Solution Implementation

1from collections import deque
2from itertools import pairwise
3from math import inf
4
5class Solution:
6    def minimum_seconds(self, land):
7        # Initialize the dimensions of the land
8        rows, cols = len(land), len(land[0])
9        # Visited matrix to keep track of visited cells
10        visited = [[False] * cols for _ in range(rows)]
11        # Grid to hold the time taken to reach each cell from lava (initialized to infinity)
12        time_grid = [[inf] * cols for _ in range(rows)]
13        # Queue for BFS algorithm
14        queue = deque()
15        # Start indices
16        start_i, start_j = 0, 0
17        # Iterating over each cell in the land
18        for i, row in enumerate(land):
19            for j, char in enumerate(row):
20                if char == "*":
21                    # Lava spotted; add to the queue
22                    queue.append((i, j))
23                if char == "S":
24                    # Start position identified
25                    start_i, start_j = i, j
26      
27        # Directions for navigating the neighbors (Up, Right, Down, Left)
28        directions = [-1, 0, 1, 0, -1]
29        # Time counter for the spread of lava
30        time_counter = 0
31        # BFS to calculate the time each cell takes to get covered by lava
32        while queue:
33            for _ in range(len(queue)):
34                i, j = queue.popleft()
35                time_grid[i][j] = time_counter
36                # Exploring all neighbors
37                for a, b in pairwise(directions):
38                    x, y = i + a, j + b
39                    # Checking if the move is valid (in bounds, not visited, and is passable terrain)
40                    if 0 <= x < rows and 0 <= y < cols and not visited[x][y] and land[x][y] in ".S":
41                        visited[x][y] = True
42                        queue.append((x, y))
43            # Incrementing time after each level of BFS
44            time_counter += 1
45      
46        # Reset variables for the second BFS to find the minimum time to the destination
47        time_counter = 0
48        queue = deque([(start_i, start_j)])
49        visited = [[False] * cols for _ in range(rows)]
50        visited[start_i][start_j] = True
51      
52        # BFS to find the minimum time to reach destination without getting caught by lava
53        while queue:
54            for _ in range(len(queue)):
55                i, j = queue.popleft()
56                if land[i][j] == "D":
57                    # Destination reached; return the time_count
58                    return time_counter
59                # Explore all valid neighbors
60                for a, b in pairwise(directions):
61                    x, y = i + a, j + b
62                    # Conditions to ensure the cell is safe to move into
63                    if (0 <= x < rows
64                            and 0 <= y < cols
65                            and time_grid[x][y] > time_counter + 1
66                            and not visited[x][y]
67                            and land[x][y] in ".D"):
68                        visited[x][y] = True
69                        queue.append((x, y))
70            # Increment time_counter after each level of BFS
71            time_counter += 1
72      
73        # If the algorithm couldn't reach the destination, return -1
74        return -1
75
1class Solution {
2
3    // Method to calculate the minimum seconds required to reach the destination
4    public int minimumSeconds(List<List<String>> land) {
5        // Dimensions of the grid
6        int rows = land.size();
7        int cols = land.get(0).size();
8
9        // Visited matrix to keep track of visited cells
10        boolean[][] visited = new boolean[rows][cols];
11
12        // Grid to maintain the time taken for water to reach each cell
13        int[][] timeToFill = new int[rows][cols];
14
15        // Queue to perform the BFS
16        Deque<int[]> queue = new ArrayDeque<>();
17      
18        // Start position coordinates
19        int startI = 0;
20        int startJ = 0;
21
22        // Initialize the time to reach every cell with a large number
23        // and populate the start coordinates and water cells
24        for (int i = 0; i < rows; ++i) {
25            Arrays.fill(timeToFill[i], Integer.MAX_VALUE);
26            for (int j = 0; j < cols; ++j) {
27                String cell = land.get(i).get(j);
28                if ("*".equals(cell)) {
29                    queue.offer(new int[]{i, j}); // Add water cells to the queue
30                } else if ("S".equals(cell)) {
31                    startI = i; // Save start position
32                    startJ = j;
33                }
34            }
35        }
36
37        // Directions array for moving up, right, down, left
38        int[] directions = {-1, 0, 1, 0, -1};
39
40        // Spread the water in the grid
41        for (int time = 0; !queue.isEmpty(); ++time) {
42            for (int k = queue.size(); k > 0; --k) {
43                int[] position = queue.poll();
44                int i = position[0], j = position[1];
45                timeToFill[i][j] = time; // Set the time to fill the current cell
46              
47                // Explore neighbors
48                for (int d = 0; d < 4; ++d) {
49                    int x = i + directions[d], y = j + directions[d + 1];
50                    if (x >= 0 && x < rows && y >= 0 && y < cols && !visited[x][y]) {
51                        boolean isEmpty = ".".equals(land.get(x).get(y));
52                        boolean isStart = "S".equals(land.get(x).get(y));
53                        if (isEmpty || isStart) {
54                            visited[x][y] = true;
55                            queue.offer(new int[]{x, y});
56                        }
57                    }
58                }
59            }
60        }
61
62        // Reset the queue and visited matrix for the second BFS to find the path
63        queue.offer(new int[]{startI, startJ});
64        visited = new boolean[rows][cols];
65        visited[startI][startJ] = true;
66      
67        // Find the minimum path to the destination
68        for (int time = 0; !queue.isEmpty(); ++time) {
69            for (int k = queue.size(); k > 0; --k) {
70                int[] position = queue.poll();
71                int i = position[0], j = position[1];
72              
73                if ("D".equals(land.get(i).get(j))) {
74                    return time; // Return the time if destination is reached
75                }
76              
77                // Explore neighbors
78                for (int d = 0; d < 4; ++d) {
79                    int x = i + directions[d], y = j + directions[d + 1];
80                    if (x >= 0 && x < rows && y >= 0 && y < cols && !visited[x][y] && timeToFill[x][y] > time + 1) {
81                        boolean isEmpty = ".".equals(land.get(x).get(y));
82                        boolean isDestination = "D".equals(land.get(x).get(y));
83                        if (isEmpty || isDestination) {
84                            visited[x][y] = true;
85                            queue.offer(new int[]{x, y});
86                        }
87                    }
88                }
89            }
90        }
91
92        // Return -1 if destination cannot be reached
93        return -1;
94    }
95}
96
1#include <vector>
2#include <queue>
3#include <cstring>
4using namespace std;
5
6class Solution {
7public:
8    int minimumSeconds(vector<vector<string>>& land) {
9        // Size of the grid
10        int rowCount = land.size(), colCount = land[0].size();
11      
12        // Visited grid to keep track of visited cells
13        bool visited[rowCount][colCount];
14      
15        // Time grid to keep track of the minimum seconds needed to reach each cell
16        int timeGrid[rowCount][colCount];
17      
18        // Initialize visited and timeGrid
19        memset(visited, false, sizeof(visited));
20        memset(timeGrid, 0x3f, sizeof(timeGrid));
21      
22        // Queue for BFS
23        queue<pair<int, int>> queue;
24      
25        // Start cell coordinates
26        int startX = 0, startY = 0;
27      
28        // Preprocess the grid to find the locations of all fire and the start
29        for (int i = 0; i < rowCount; ++i) {
30            for (int j = 0; j < colCount; ++j) {
31                auto cell = land[i][j];
32                if (cell == "*") { // Fire location
33                    queue.emplace(i, j);
34                } else if (cell == "S") { // Start location
35                    startX = i;
36                    startY = j;
37                }
38            }
39        }
40      
41        // Directions arrays for moving up, right, down, left
42        int dirs[5] = {-1, 0, 1, 0, -1};
43      
44        // BFS for fire propagation
45        for (int t = 0; !queue.empty(); ++t) {
46            for (int k = queue.size(); k; --k) {
47                auto [i, j] = queue.front();
48                queue.pop();
49              
50                // Mark the time it takes for fire to reach (i, j)
51                timeGrid[i][j] = t;
52              
53                // Explore adjacent cells to propagate fire
54                for (int d = 0; d < 4; ++d) {
55                    int nextX = i + dirs[d], nextY = j + dirs[d + 1];
56                    if (nextX >= 0 && nextX < rowCount && nextY >= 0 && nextY < colCount && !visited[nextX][nextY]) {
57                        bool isEmpty = land[nextX][nextY] == ".";
58                        bool isStart = land[nextX][nextY] == "S";
59                        if (isEmpty || isStart) {
60                            visited[nextX][nextY] = true;
61                            queue.emplace(nextX, nextY);
62                        }
63                    }
64                }
65            }
66        }
67      
68        // Start BFS from the starting location
69        queue.emplace(startX, startY);
70        memset(visited, false, sizeof(visited));
71        visited[startX][startY] = true;
72      
73        // BFS to find the shortest path to destination avoiding fire
74        for (int t = 0; !queue.empty(); ++t) {
75            for (int k = queue.size(); k; --k) {
76                auto [i, j] = queue.front();
77                queue.pop();
78              
79                // Destination found, return time
80                if (land[i][j] == "D") {
81                    return t;
82                }
83              
84                // Explore adjacent cells
85                for (int d = 0; d < 4; ++d) {
86                    int nextX = i + dirs[d], nextY = j + dirs[d + 1];
87                    if (nextX >= 0 && nextX < rowCount && nextY >= 0 && nextY < colCount && 
88                        !visited[nextX][nextY] && timeGrid[nextX][nextY] > t + 1) {
89                        // Check if it's either an empty cell or the destination
90                        bool isEmpty = land[nextX][nextY] == ".";
91                        bool isDest = land[nextX][nextY] == "D";
92                        if (isEmpty || isDest) {
93                            visited[nextX][nextY] = true;
94                            queue.emplace(nextX, nextY);
95                        }
96                    }
97                }
98            }
99        }
100      
101        // If destination is not found, return -1
102        return -1;
103    }
104};
105
1// Function to find the minimum seconds required to reach the destination 'D'
2// starting from 'S' without being caught by 'guards' (*).
3// 'land' represents the 2D grid with '.' as open spaces, '*' as guards, 'S' as start, and 'D' as destination.
4function minimumSeconds(land: string[][]): number {
5    const rows = land.length;
6    const cols = land[0].length;
7
8    // 'grid' stores the time taken for guards to reach each cell.
9    const grid: number[][] = Array.from(
10        { length: rows },
11        () => new Array(cols).fill(Number.MAX_SAFE_INTEGER)
12    );
13
14    // 'visited' marks whether a cell has been visited or not to prevent revisiting.
15    const visited: boolean[][] = Array.from(
16        { length: rows },
17        () => new Array(cols).fill(false)
18    );
19
20    // Queue to perform BFS (Breadth-First Search).
21    const queue: number[][] = [];
22
23    // Starting position.
24    let startI = 0;
25    let startJ = 0;
26
27    // Initialize the queue with guards' positions and find the start position.
28    for (let i = 0; i < rows; ++i) {
29        for (let j = 0; j < cols; ++j) {
30            const cell = land[i][j];
31            if (cell === '*') {
32                queue.push([i, j]);
33            } else if (cell === 'S') {
34                [startI, startJ] = [i, j];
35            }
36        }
37    }
38
39    // Directions for moving up, right, down, left.
40    const directions = [-1, 0, 1, 0, -1];
41
42    // First BFS to spread the guards through the land.
43    for (let time = 0; queue.length; ++time) {
44        for (let k = queue.length; k; --k) {
45            const [currentI, currentJ] = queue.shift()!;
46            grid[currentI][currentJ] = time;
47            for (let d = 0; d < 4; ++d) {
48                const nextI = currentI + directions[d];
49                const nextJ = currentJ + directions[d + 1];
50                if (nextI >= 0 && nextI < rows &&
51                    nextJ >= 0 && nextJ < cols &&
52                    !visited[nextI][nextJ] &&
53                    'S.'.includes(land[nextI][nextJ])) {
54                    visited[nextI][nextJ] = true;
55                    queue.push([nextI, nextJ]);
56                }
57            }
58        }
59    }
60
61    // Second BFS to move from 'S' to 'D' while avoiding the guards.
62    queue.push([startI, startJ]);
63    for (let i = 0; i < rows; ++i) {
64        visited[i].fill(false);
65    }
66    visited[startI][startJ] = true;
67    for (let time = 0; queue.length; ++time) {
68        for (let k = queue.length; k; --k) {
69            const [currentI, currentJ] = queue.shift()!;
70            if (land[currentI][currentJ] === 'D') {
71                return time; // Destination found.
72            }
73            for (let d = 0; d < 4; ++d) {
74                const nextI = currentI + directions[d];
75                const nextJ = currentJ + directions[d + 1];
76                if (nextI >= 0 && nextI < rows &&
77                    nextJ >= 0 && nextJ < cols &&
78                    !visited[nextI][nextJ] &&
79                    grid[nextI][nextJ] > time + 1 &&
80                    'D.'.includes(land[nextI][nextJ])) {
81                    visited[nextI][nextJ] = true;
82                    queue.push([nextI, nextJ]);
83                }
84            }
85        }
86    }
87    return -1; // No path found to the destination.
88}
89
Not Sure What to Study? Take the 2-min Quiz:

Which data structure is used to implement priority queue?

Time and Space Complexity

The given Python code snippet is for a flood-fill algorithm, which is used to determine the minimum number of seconds to reach the destination from the start in a grid-based land area with various terrains indicated by different characters.

Time Complexity:

The time complexity of the flood-fill algorithm mainly depends on the number of cells in the grid, which is m * n, where m is the number of rows and n is the number of columns. The code performs two breadth-first searches (BFS).

The first BFS is to calculate the time (t) it takes for water (*) to reach each cell. It runs through all the cells containing water to fill up the grid g with the times. Since each cell is visited once, this BFS is O(m * n).

The second BFS starts at the start position (S) and checks for the shortest safe path to the destination (D). Again, each cell is examined once at most, so this BFS is also O(m * n).

Given that these BFS runs are sequential and we don't have recursive BFS calls, the overall time complexity of the code would be O(m * n) for the two BFS operations.

Space Complexity:

The space complexity is determined by the amount of space taken up by the data structures used in the algorithm.

The vis and g matrices both take up O(m * n) space. Additionally, the BFS queue can store up to all the cells in the worst case, so it also requires O(m * n) space.

The space used for auxiliary variables is constant and does not scale with m or n, so it can be considered O(1).

Therefore, the total space complexity of the code is O(m * n) due to the storage required for vis, g, and the BFS queue.

Note that the space complexity deduced here is assuming an optimal implementation of Python's deque. Some implementations may have overheads that vary based on specific factors like memory allocation patterns and internal workings of deque which are not explicitly considered here.

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

Fast Track Your Learning with Our Quick Skills Quiz:

Depth first search can be used to find whether two components in a graph are connected.


Recommended Readings


Got a question? Ask the Teaching Assistant anything you don't understand.

Still not clear? Ask in the Forum,  Discord or Submit the part you don't understand to our editors.


TA 👨‍🏫