1970. Last Day Where You Can Still Cross
Problem Description
You have a grid/matrix with row
rows and col
columns. Initially (day 0), the entire grid is land (represented by 0). Each day, one cell gets flooded with water (changes from 0 to 1).
You're given an array cells
where cells[i] = [ri, ci]
tells you that on day i
, the cell at row ri
and column ci
will be flooded with water. Note that all coordinates are 1-based (rows and columns start from 1, not 0).
Your task is to find the last day when you can still walk from any cell in the top row to any cell in the bottom row by only stepping on land cells (cells with value 0). You can move in four directions: up, down, left, and right.
For example:
- If you have a 2Γ2 grid and
cells = [[1,1], [2,1], [1,2], [2,2]]
- Day 0: All cells are land
- Day 1: Cell (1,1) becomes water
- Day 2: Cell (2,1) becomes water
- Day 3: Cell (1,2) becomes water
- Day 4: Cell (2,2) becomes water
You need to determine on which day you can no longer traverse from the top row to the bottom row while staying on land.
The solution uses binary search combined with BFS. Since if you can cross on day k
, you can also cross on any earlier day (monotonic property), binary search helps find the maximum day efficiently. For each candidate day mid
, the algorithm floods the first mid
cells and checks if a path still exists from top to bottom using BFS.
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 a grid/matrix where we need to find paths. A grid can be viewed as a graph where each cell is a node and adjacent cells are connected by edges.
Is it a tree?
- No: The grid structure is not a tree. It's a general graph that can have multiple paths between nodes and may contain cycles (you can move in 4 directions and potentially return to the same cell).
Is the problem related to directed acyclic graphs?
- No: The grid allows movement in all four directions (bidirectional), and cycles are possible, so it's not a DAG.
Is the problem related to shortest paths?
- No: We're not looking for the shortest path. We just need to determine if ANY path exists from the top row to the bottom row.
Does the problem involve connectivity?
- Yes: The core of the problem is checking connectivity - specifically, whether there exists a connected path from any cell in the top row to any cell in the bottom row through land cells only.
Disjoint Set Union
- While DSU could be used here, the solution uses BFS instead. However, following the flowchart strictly would lead us to DSU.
Alternative Path - Small Constraints:
- Looking at the problem differently, if we consider the search space has manageable constraints (the grid size and number of days to check), we could also arrive at:
Does the problem have small constraints?
- Yes (relatively): While not explicitly small, the binary search reduces the search space significantly.
DFS/backtracking
- Yes: DFS could be used to explore paths from top to bottom row. The solution uses BFS, but DFS would work equally well for checking connectivity.
Conclusion: The flowchart suggests using either DSU for connectivity or DFS/backtracking for path exploration. The actual solution combines binary search with BFS (which is interchangeable with DFS for this connectivity check). DFS would traverse the land cells recursively to determine if a path exists from top to bottom row.
Intuition
The key insight is recognizing the monotonic property of this problem. If you can walk from top to bottom on day k
, you can definitely walk on any earlier day because fewer cells would be flooded. Conversely, if you cannot walk on day k
, you cannot walk on any later day either because more cells will be flooded.
This monotonic behavior immediately suggests binary search. Instead of checking every single day sequentially (which would be inefficient), we can use binary search to find the exact turning point - the last day when crossing is still possible.
Think of it like this: imagine you're watching a time-lapse video of an island slowly sinking into the ocean. At first, there are many paths from north to south. As more land floods each day, paths start disappearing. Eventually, there comes a specific day when the last possible path gets cut off. We want to find that exact day.
For any given day mid
, we need to check if a path exists. We do this by:
- Flooding all cells that should be water by day
mid
(the firstmid
cells from thecells
array) - Checking if we can still traverse from top to bottom
The traversal check is a classic graph connectivity problem. We can use either DFS or BFS to explore from any starting cell in the top row and see if we can reach any cell in the bottom row. The solution uses BFS, but DFS would work equally well.
The binary search bounds are straightforward:
- Minimum: day 1 (we know day 0 always works since everything is land)
- Maximum: total number of cells (worst case, all cells get flooded)
By combining binary search with path-finding (BFS/DFS), we efficiently find the answer in O(row Γ col Γ log(row Γ col))
time instead of checking every single day sequentially.
Learn more about Depth-First Search, Breadth-First Search, Union Find and Binary Search patterns.
Solution Approach
The solution implements binary search combined with BFS to find the last day we can cross from top to bottom.
Binary Search Setup:
- Initialize search bounds:
l = 1
(left) andr = row * col
(right) - We search for the maximum day
k
where crossing is still possible - Use the variant
mid = (l + r + 1) >> 1
to avoid infinite loops whenl
andr
are adjacent
The Check Function:
For each candidate day k
, we need to verify if crossing is possible:
-
Build the grid state at day k:
- Create a 2D grid
g
initialized with all 0s (land) - Mark the first
k
cells fromcells
array as water (1) - Convert 1-based coordinates to 0-based:
g[i-1][j-1] = 1
- Create a 2D grid
-
BFS from top row:
- Initialize queue
q
with all land cells from the top row:(0, j)
whereg[0][j] == 0
- These are all possible starting points
- Initialize queue
-
BFS exploration:
- For each cell
(x, y)
in the queue:- Check if we've reached the bottom row:
if x == row - 1: return True
- Explore all 4 directions using
dirs = (-1, 0, 1, 0, -1)
pattern - The
pairwise(dirs)
generates pairs:(-1, 0)
,(0, 1)
,(1, 0)
,(0, -1)
representing up, right, down, left - For each valid neighbor that's still land:
- Add to queue:
q.append((nx, ny))
- Mark as visited by setting
g[nx][ny] = 1
(prevents revisiting)
- Add to queue:
- Check if we've reached the bottom row:
- For each cell
-
Return result:
- If we reach bottom row during BFS: return
True
- If BFS completes without reaching bottom: return
False
- If we reach bottom row during BFS: return
Binary Search Logic:
- If
check(mid)
returnsTrue
: we can still cross on daymid
- Update
l = mid
(search for a later day)
- Update
- If
check(mid)
returnsFalse
: we cannot cross on daymid
- Update
r = mid - 1
(search for an earlier day)
- Update
- Continue until
l == r
, which gives us the last valid day
Time Complexity: O(row Γ col Γ log(row Γ col))
- Binary search iterations:
O(log(row Γ col))
- Each check function:
O(row Γ col)
for BFS traversal
Space Complexity: O(row Γ col)
for the grid and BFS queue
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example with a 3Γ3 grid and cells = [[1,2], [2,2], [3,2], [1,1], [3,3]]
.
Initial Setup:
- Grid dimensions: 3 rows Γ 3 columns
- Binary search bounds:
l = 1
,r = 9
(though we only have 5 cells to flood) - We'll search for the last day we can walk from row 1 to row 3
Binary Search Iteration 1:
mid = (1 + 9 + 1) >> 1 = 5
- Check if we can cross on day 5 (all 5 cells flooded)
- Flood cells: [1,2], [2,2], [3,2], [1,1], [3,3]
- Grid state after flooding:
1 1 0 0 1 0 0 1 1
- BFS from top row: Start from (0,2) - the only land cell in top row
- BFS explores: (0,2) β cannot reach any neighbors that lead to bottom
- Result: Cannot cross! Update
r = mid - 1 = 4
Binary Search Iteration 2:
mid = (1 + 4 + 1) >> 1 = 3
- Check if we can cross on day 3 (first 3 cells flooded)
- Flood cells: [1,2], [2,2], [3,2]
- Grid state:
0 1 0 0 1 0 0 1 0
- BFS from top row: Start from (0,0) and (0,2)
- From (0,0): explore (0,0) β (1,0) β (2,0) - reached bottom row!
- Result: Can cross! Update
l = mid = 3
Binary Search Iteration 3:
mid = (3 + 4 + 1) >> 1 = 4
- Check if we can cross on day 4 (first 4 cells flooded)
- Flood cells: [1,2], [2,2], [3,2], [1,1]
- Grid state:
1 1 0 0 1 0 0 1 0
- BFS from top row: Start from (0,2)
- From (0,2): explore (0,2) β (1,2) is water, cannot proceed
- Try going around: no valid path to bottom exists
- Result: Cannot cross! Update
r = mid - 1 = 3
Final Result:
l == r == 3
- The last day we can cross from top to bottom is day 3
This demonstrates how binary search efficiently narrows down the answer by checking strategic midpoints rather than testing every single day sequentially.
Solution Implementation
1class Solution:
2 def latestDayToCross(self, row: int, col: int, cells: List[List[int]]) -> int:
3 def can_cross(day: int) -> bool:
4 """
5 Check if it's possible to cross from top to bottom on the given day.
6
7 Args:
8 day: Number of cells that have been filled with water
9
10 Returns:
11 True if crossing is possible, False otherwise
12 """
13 # Initialize grid (0 = land, 1 = water)
14 grid = [[0] * col for _ in range(row)]
15
16 # Fill cells with water for the first 'day' days
17 for i in range(day):
18 r, c = cells[i]
19 grid[r - 1][c - 1] = 1 # Convert to 0-indexed
20
21 # BFS queue: start from all land cells in the top row
22 queue = []
23 for j in range(col):
24 if grid[0][j] == 0: # If top row cell is land
25 queue.append((0, j))
26 grid[0][j] = 1 # Mark as visited
27
28 # BFS to find path from top to bottom
29 idx = 0
30 while idx < len(queue):
31 x, y = queue[idx]
32 idx += 1
33
34 # Check if we reached the bottom row
35 if x == row - 1:
36 return True
37
38 # Explore all 4 directions
39 for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
40 next_x, next_y = x + dx, y + dy
41
42 # Check bounds and if cell is unvisited land
43 if (0 <= next_x < row and
44 0 <= next_y < col and
45 grid[next_x][next_y] == 0):
46 queue.append((next_x, next_y))
47 grid[next_x][next_y] = 1 # Mark as visited
48
49 return False
50
51 # Binary search for the latest day we can still cross
52 left, right = 1, row * col
53
54 while left < right:
55 # Use ceiling division to avoid infinite loop
56 mid = (left + right + 1) // 2
57
58 if can_cross(mid):
59 # Can still cross on day 'mid', try later days
60 left = mid
61 else:
62 # Cannot cross on day 'mid', try earlier days
63 right = mid - 1
64
65 return left
66
1class Solution {
2 private int[][] cells;
3 private int rows;
4 private int cols;
5
6 /**
7 * Finds the latest day we can walk from top to bottom of the grid
8 * @param row number of rows in the grid
9 * @param col number of columns in the grid
10 * @param cells array of cells that become water on each day (1-indexed)
11 * @return the latest day we can still cross from top to bottom
12 */
13 public int latestDayToCross(int row, int col, int[][] cells) {
14 // Binary search on the number of days
15 int left = 1;
16 int right = cells.length;
17 this.cells = cells;
18 this.rows = row;
19 this.cols = col;
20
21 // Binary search to find the maximum day we can still cross
22 while (left < right) {
23 int mid = (left + right + 1) >> 1; // Equivalent to (left + right + 1) / 2
24 if (canCross(mid)) {
25 // If we can cross on day mid, try a later day
26 left = mid;
27 } else {
28 // If we cannot cross on day mid, try an earlier day
29 right = mid - 1;
30 }
31 }
32 return left;
33 }
34
35 /**
36 * Checks if we can cross from top row to bottom row after k days
37 * @param dayCount number of days (cells that have become water)
38 * @return true if crossing is possible, false otherwise
39 */
40 private boolean canCross(int dayCount) {
41 // Initialize grid: 0 = land, 1 = water or visited
42 int[][] grid = new int[rows][cols];
43
44 // Mark cells as water for the first dayCount days
45 for (int i = 0; i < dayCount; i++) {
46 // Convert from 1-indexed to 0-indexed
47 grid[cells[i][0] - 1][cells[i][1] - 1] = 1;
48 }
49
50 // Direction vectors for moving up, right, down, left
51 final int[] directions = {-1, 0, 1, 0, -1};
52
53 // BFS queue to store positions to explore
54 Deque<int[]> queue = new ArrayDeque<>();
55
56 // Start BFS from all land cells in the top row
57 for (int col = 0; col < cols; col++) {
58 if (grid[0][col] == 0) { // If it's land
59 queue.offer(new int[] {0, col});
60 grid[0][col] = 1; // Mark as visited
61 }
62 }
63
64 // Perform BFS to find if we can reach the bottom row
65 while (!queue.isEmpty()) {
66 int[] position = queue.poll();
67 int currentRow = position[0];
68 int currentCol = position[1];
69
70 // Check if we've reached the bottom row
71 if (currentRow == rows - 1) {
72 return true;
73 }
74
75 // Explore all 4 adjacent cells
76 for (int i = 0; i < 4; i++) {
77 int nextRow = currentRow + directions[i];
78 int nextCol = currentCol + directions[i + 1];
79
80 // Check if the next position is valid and is unvisited land
81 if (nextRow >= 0 && nextRow < rows &&
82 nextCol >= 0 && nextCol < cols &&
83 grid[nextRow][nextCol] == 0) {
84
85 queue.offer(new int[] {nextRow, nextCol});
86 grid[nextRow][nextCol] = 1; // Mark as visited
87 }
88 }
89 }
90
91 // Cannot reach the bottom row
92 return false;
93 }
94}
95
1class Solution {
2public:
3 int latestDayToCross(int row, int col, vector<vector<int>>& cells) {
4 // Binary search on the number of days
5 int left = 1, right = cells.size();
6
7 // Direction arrays for moving up, right, down, left
8 int directions[5] = {0, 1, 0, -1, 0};
9
10 // Lambda function to check if we can cross from top to bottom on day k
11 auto canCross = [&](int day) -> bool {
12 // Initialize grid: 0 = land (passable), 1 = water (blocked)
13 int grid[row][col];
14 memset(grid, 0, sizeof(grid));
15
16 // Fill water cells for the first 'day' days
17 for (int i = 0; i < day; ++i) {
18 grid[cells[i][0] - 1][cells[i][1] - 1] = 1;
19 }
20
21 // BFS queue to store positions
22 queue<pair<int, int>> bfsQueue;
23
24 // Start BFS from all land cells in the top row
25 for (int j = 0; j < col; ++j) {
26 if (grid[0][j] == 0) {
27 bfsQueue.emplace(0, j);
28 grid[0][j] = 1; // Mark as visited
29 }
30 }
31
32 // Perform BFS to find path to bottom row
33 while (!bfsQueue.empty()) {
34 auto [currentRow, currentCol] = bfsQueue.front();
35 bfsQueue.pop();
36
37 // Check if we reached the bottom row
38 if (currentRow == row - 1) {
39 return true;
40 }
41
42 // Explore all 4 adjacent cells
43 for (int i = 0; i < 4; ++i) {
44 int nextRow = currentRow + directions[i];
45 int nextCol = currentCol + directions[i + 1];
46
47 // Check boundaries and if the cell is unvisited land
48 if (nextRow >= 0 && nextRow < row &&
49 nextCol >= 0 && nextCol < col &&
50 grid[nextRow][nextCol] == 0) {
51 bfsQueue.emplace(nextRow, nextCol);
52 grid[nextRow][nextCol] = 1; // Mark as visited
53 }
54 }
55 }
56
57 return false;
58 };
59
60 // Binary search to find the latest day we can still cross
61 while (left < right) {
62 int mid = (left + right + 1) >> 1;
63
64 if (canCross(mid)) {
65 left = mid; // We can cross on day mid, try later days
66 } else {
67 right = mid - 1; // Cannot cross on day mid, try earlier days
68 }
69 }
70
71 return left;
72 }
73};
74
1/**
2 * Finds the last day where it's possible to walk from top row to bottom row
3 * @param row - Number of rows in the grid
4 * @param col - Number of columns in the grid
5 * @param cells - Array of cells that become water on each day (1-indexed)
6 * @returns The last day (1-indexed) where crossing is still possible
7 */
8function latestDayToCross(row: number, col: number, cells: number[][]): number {
9 // Binary search range: minimum 1 day, maximum all cells
10 let left: number = 1;
11 let right: number = cells.length;
12
13 /**
14 * Checks if it's possible to cross from top to bottom on day k
15 * @param dayNumber - The day to check (number of cells that have become water)
16 * @returns True if crossing is possible, false otherwise
17 */
18 const canCrossOnDay = (dayNumber: number): boolean => {
19 // Initialize grid: 0 = land, 1 = water or visited
20 const grid: number[][] = Array.from({ length: row }, () => Array(col).fill(0));
21
22 // Mark cells as water for the first k days
23 for (let i = 0; i < dayNumber; ++i) {
24 const [cellRow, cellCol] = cells[i];
25 // Convert from 1-indexed to 0-indexed
26 grid[cellRow - 1][cellCol - 1] = 1;
27 }
28
29 // BFS queue to find path from top to bottom
30 const queue: number[][] = [];
31
32 // Start BFS from all accessible cells in the top row
33 for (let j = 0; j < col; ++j) {
34 if (grid[0][j] === 0) {
35 queue.push([0, j]);
36 grid[0][j] = 1; // Mark as visited
37 }
38 }
39
40 // Direction vectors for moving up, right, down, left
41 const directions: number[] = [-1, 0, 1, 0, -1];
42
43 // Process BFS queue
44 for (const [currentRow, currentCol] of queue) {
45 // Check if we reached the bottom row
46 if (currentRow === row - 1) {
47 return true;
48 }
49
50 // Explore all 4 adjacent cells
51 for (let i = 0; i < 4; ++i) {
52 const nextRow: number = currentRow + directions[i];
53 const nextCol: number = currentCol + directions[i + 1];
54
55 // Check if next cell is valid and unvisited land
56 if (nextRow >= 0 && nextRow < row &&
57 nextCol >= 0 && nextCol < col &&
58 grid[nextRow][nextCol] === 0) {
59 queue.push([nextRow, nextCol]);
60 grid[nextRow][nextCol] = 1; // Mark as visited
61 }
62 }
63 }
64
65 return false;
66 };
67
68 // Binary search for the last day where crossing is possible
69 while (left < right) {
70 // Calculate mid point, biased towards upper half
71 const mid: number = (left + right + 1) >> 1;
72
73 if (canCrossOnDay(mid)) {
74 // Can still cross on day mid, try later days
75 left = mid;
76 } else {
77 // Cannot cross on day mid, try earlier days
78 right = mid - 1;
79 }
80 }
81
82 return left;
83}
84
Time and Space Complexity
Time Complexity: O(m Γ n Γ log(m Γ n))
The algorithm uses binary search on the range [1, m Γ n]
, which requires O(log(m Γ n))
iterations. For each binary search iteration, the check
function is called, which performs a BFS traversal on an m Γ n
grid. The BFS visits each cell at most once, taking O(m Γ n)
time. Therefore, the overall time complexity is O(m Γ n Γ log(m Γ n))
.
Space Complexity: O(m Γ n)
The space complexity is dominated by:
- The grid
g
created in thecheck
function:O(m Γ n)
- The queue
q
used for BFS, which in the worst case can contain all cells:O(m Γ n)
- The
cells
array is given as input and not counted in auxiliary space
Therefore, the total space complexity is O(m Γ n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Off-by-One Error in Binary Search
The most critical pitfall is using the wrong binary search formula when searching for the maximum valid value. Using mid = (left + right) // 2
instead of mid = (left + right + 1) // 2
causes an infinite loop.
Why it happens:
- When
left = k
andright = k + 1
, using floor division givesmid = k
- If
can_cross(k)
returnsTrue
, we setleft = k
(unchanged) - This creates an infinite loop since the bounds never converge
Solution:
Always use mid = (left + right + 1) // 2
when:
- Searching for maximum valid value
- Updating with
left = mid
when condition is met
2. Coordinate System Confusion (1-based vs 0-based)
The problem provides 1-based coordinates in the cells
array, but Python arrays are 0-based. Forgetting to convert leads to IndexError or wrong cells being flooded.
Incorrect:
grid[r][c] = 1 # Direct use of 1-based coordinates
Correct:
grid[r - 1][c - 1] = 1 # Convert to 0-based
3. Inefficient BFS Implementation
Using collections.deque
with popleft()
or list with pop(0)
creates unnecessary overhead.
Less efficient:
from collections import deque queue = deque() while queue: x, y = queue.popleft() # O(1) but with deque overhead
More efficient for this problem:
queue = []
idx = 0
while idx < len(queue):
x, y = queue[idx]
idx += 1 # Simple index advancement, no removal needed
4. Not Marking Cells as Visited Immediately
Marking cells as visited only after dequeuing (instead of when enqueuing) can cause the same cell to be added multiple times, leading to TLE.
Incorrect:
while queue: x, y = queue[idx] grid[x][y] = 1 # Mark visited after dequeuing - TOO LATE! for dx, dy in directions: if valid and grid[nx][ny] == 0: queue.append((nx, ny))
Correct:
for dx, dy in directions: if valid and grid[nx][ny] == 0: queue.append((nx, ny)) grid[nx][ny] = 1 # Mark visited immediately when enqueuing
5. Binary Search Bounds Initialization
Starting with left = 0
instead of left = 1
wastes an iteration since day 0 means no cells are flooded (always crossable).
Suboptimal:
left, right = 0, row * col
Optimal:
left, right = 1, row * col
What is the best way of checking if an element exists in an unsorted array once in terms of time complexity? Select the best that applies.
Recommended Readings
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Union Find Disjoint Set Union Data Structure Introduction Prerequisite Depth First Search Review problems dfs_intro Once we have a strong grasp of recursion and Depth First Search we can now introduce Disjoint Set Union DSU This data structure is motivated by the following problem Suppose we have sets of elements
Want a Structured Path to Master System Design Too? Donβt Miss This!