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.
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 problem involves finding a path in a grid, which can be treated as a graph where each cell is a node, and edges exist between adjacent allowed moves.
Is it a tree?
- No: Given that the grid isn't strictly hierarchical and can have various paths between nodes, it's not a tree.
Is the problem related to directed acyclic graphs (DAGs)?
- No: The grid allows movement in more than one direction and isn't specifically directed or acyclic, relating more to potential cyclic paths due to multiple routes.
Is the problem related to shortest paths?
- Yes: The objective is to reach the destination in the minimum time, which constitutes finding a shortest path in terms of time required.
Is the graph weighted?
- Yes: The traversal time between different nodes varies, which implies the edges have weights (different costs or times to traverse).
Using Dijkstra's Algorithm?
- Given that the problem involves finding paths through water without drowning, where waiting times (increased costs) may be employed to avoid obstacles, and considering that these weights are varied, it aligns well with using Dijkstra’s Algorithm for such a weighted graph with shortest path concerns.
Conclusion: The flowchart, when followed precisely for this problem scenario, suggests using BFS for a shortest path in an unweighted scenario. However, because the problem involves weighted paths due to varied traversal times and potential waiting, Dijkstra’s Algorithm, which is effective for weighted graphs, would be more suitable here. If strictly following the flowchart to the end for an unweighted scenario and assuming uniform costs, BFS (Breadth-First Search) would be recognized; however, adapting to the specific case details implies diverging at the "Is the graph weighted?" query with an answer leading toward a more fitting approach via Dijkstra's Algorithm. Therefore, revisiting the "weighted" node, BFS is listed under the "no" branch, which is not suitable here due to the weighted nature described.
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.
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.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's illustrate the solution approach with a small example grid:
S.. .*. ..D
Here, "S"
is our starting position, "."
represents open cells, "*"
represents an initially flooded cell, and "D"
is our destination.
- Initialization of Flood Time and Visited Grids: Our grid
g
to track flooding times would initially be set withinf
, andvis
grid is allFalse
.
g = [[inf, inf, inf], [inf, 0, inf], [inf, inf, inf]] vis = [[False, False, False], [False, True, False], [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.
- First BFS (Flood Spread):
- Start with the positions marked with
"*"
; in this case, it's the cell at(1, 1)
. Queueq
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 ing
to be one more than the current cell and mark it as visited, then add it to the queue.
- Start with the positions marked with
As a result, after spreading, the grid will have the following flooding times:
g = [[1, 2, inf], [1, 0, 1], [inf, 2, inf]]
- Second BFS (Finding Path):
- Reset our
vis
grid to allFalse
and place the starting position(0, 0)
onto our queueq
:[(0, 0)]
. - Perform a BFS from that position, only moving to cells that are not flooded (
"X"
), not about to be flooded (according to ourg
grid and timet
), 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.
- Reset our
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 time2
. 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:
Path = S->(0, 1) Time = 1
- Then from
(0, 1)
, we can move to(1, 2)
which is safe since it will be flooded at time1
, and by the time we want to step there, it will be time2
.
Path = S->(0, 1)->(1, 2) Time = 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.
Path = S->(0, 1)->(1, 2)->(2, 2) Time = 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
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.
Which technique can we use to find the middle of a linked list?
Recommended Readings
https algomonster s3 us east 2 amazonaws com 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
LeetCode Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way we
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https algomonster s3 us east 2 amazonaws com recursion jpg You first
Want a Structured Path to Master System Design Too? Don’t Miss This!