Solution: Last Day Where You Can Still Cross
Let's solve the Last Day Where You Can Still Cross problem using the Union Find pattern.
We'll cover the following
Statement#
You are given two integers, rows and cols, which represent the number of rows and columns in a 1-based binary matrix. In this matrix, each represents land, and each represents water.
Initially, on day 0, the whole matrix will just be all 0s, that is, all land. With each passing day, one of the cells of this matrix will get flooded and, therefore, will change to water, that is, from to . This continues until the entire matrix is flooded. You are given a 1-based array, water_cells, that records which cell will be flooded on each day. Each element indicates the cell present at the row and column of the matrix that will change from land to water on the day.
We can cross any cell of the matrix as long as it’s land. Once it changes to water, we can’t cross it. To cross any cell, we can only move in one of the four cardinal directions. Given the number of rows and columns of a 1-based binary matrix and a 1-based array, water_cells, you are required to find the last day where you can still cross the matrix, from top to bottom, by walking over the land cells only.
Note: You can start from any cell in the top row, and you need to be able to reach just one cell in the bottom row for it to count as a crossing.
Constraints:
-
rowscols -
rowscols water_cells.lengthrowscols-
rows -
cols - All values of
water_cellsare unique.
Solution#
Our aim is to find the maximum number of days where we can cross a 1-based binary matrix, let’s say, matrix, while it is being flooded one cell at a time. We need to cross matrix from top to bottom using a continuous path consisting only of land cells. If, at any time, we encounter a series of connected water cells from the leftmost side of the matrix to its rightmost side that blocks our continuous path of land cells, we won’t be able to cross the matrix.
In our solution, we’ll look for a single component of connected water cells from the left to the right of matrix, on each day. Once encountered, we’ll return this day as the last day where we can still cross matrix.
Note: We are not subtracting 1 from our final answer, because, in the context of this problem, we are starting from day 0, so we don’t need to adjust the value of days.
Since we are required to find a series of connected water cells, we can consider the cells in the matrix to be vertexes in a graph—each vertex connected to its neighbors—and then try to identify connected components consisting only of water cells using union find. The union find pattern is designed to group elements into sets based on a specified property. The pattern uses a disjoint set data structure, such as an array, to keep track of which set each element belongs to. In our case, there are two properties that define whether any two cells should be grouped together:
- They both need to be water cells.
- There must be a path between them consisting only of water cells, such that the movement from one water cell to its neighboring water cell is along one of eight directions: up, down, left, right, up-left, up-right, down-left, and down-right.
Note: As per the problem statement, there is a restriction on the connectivity of land cells. A valid path of land cells will only contain land cells connected to one of its four neighboring land cells, not eight. However, when checking whether two water cells are connected, we need to take into account eight directions, as explained below.
The most obvious way to block all paths from top to bottom is for all the cells in any row to be flooded. For example, in the matrix on the right, flooding the second row does the job.
However, this isn’t the only way to stop all the paths from the top row to the bottom.
Focusing on each column individually, we might think that flooding at least one cell in each column would be enough, yet as we can see in the matrix on the right, this doesn’t quite work. There are two paths to the bottom row from cell : via cell or via cell .
We have blocked the path to cell by flooding cell , and we can similarly flood cell to block the path to cell . Here, the water cells and are neighbors, not along a cardinal direction, but along an ordinal or intercardinal direction. The same applies to cells and .
This illustrates why, when checking whether two water cells are connected, we need to check in eight directions: up, down, left, right, up-left, up-right, down-left, and down-right.
Now that we have understood why the connectivity of water cells needs to be checked along eight directions, we can use the disjoint set data structure to track which water cells are connected, i.e., which water cells belong to the same connected component.
To use the union find pattern, we create and use the UnionFind class that has two primary methods:
-
union(node1, node2): This merges the sets ofnode1andnode2into one by changing the representative ofnode1to the representative ofnode2. -
find(node): This finds and returns the representative of the set that contains the given node. It uses path compression, which speeds up the subsequent find operations by changing the parent of the visited node to the representative of the subset. This reduces the length of the path of that node to the representative, ensuring we don’t have to traverse all the intermediate nodes on future find operations.
Note: In terms of the tree representation of disjoint set union (DSU) data structure, a representative is an element in each set that resides at the root of the tree and represents that specific set.
Our solution will first initialize a variable, day, to , since we are starting from day . Next, we’ll create a rows cols grid, called matrix, with all cells initialized to s (since none of the cells have been flooded on day ). Then, proceeding from day , we’ll start filling the matrix with water cells as per the given water_cells array. The value in each cell in matrix, identified by the current entry in water_cells, is changed from to . For example, if we are on day and the value of water_cells[2] is , we will set the value of matrix[1][2] to , indicating that it is now flooded with water.
Since we are solving this problem using the union find pattern, we initialize a UnionFind object, which results in the creation of a custom disjoint set data structure: an array, reps, of size (rows * cols) + 2. The array reps is essentially a 1D array representation of matrix. The two additional elements in reps represent two extra virtual nodes—one at the leftmost side of the matrix and the other at the rightmost side of the matrix. To operate this mapping, we use a helper function, find_index(), which converts the coordinates of a cell in matrix to its corresponding unique index in reps. Initially, every element is its own representative, so the value at every index in reps is the index itself. During the execution of the union operation, when two elements are united, their representatives are set to the same value.
We use the virtual nodes to efficiently check whether the first and the last columns of matrix are connected. In simpler words, these nodes help identify whether we have a series of connected water cells from the left to the right side of matrix. If a water cell is present in the first column of matrix, we use the union operation to set the left virtual node as its representative. Similarly, if a water cell is in the last column, we use the union operation to set the right virtual node as its representative.
Every day, we flood another cell in the matrix, changing its value to . At this point, we check whether any of its eight neighboring cells is also a water cell. If it is, we use the union operation to connect it with the recently flooded cell. The union operation will connect both the cells by making their representatives the same in the array, reps. As mentioned above, if the recently flooded cell is at the left or right boundary of matrix, it will connect with the corresponding virtual node.
Next, we check to see if the flooding of this cell has created a continuous component connecting the leftmost side of matrix with its rightmost side. For this purpose, we use the find operation to check whether the left and right virtual nodes are connected, that is, whether or not they have the same representative.
If the two sides are connected, we can no longer cross the matrix from top to bottom, and the current value of day is our answer. Otherwise, we increment the value of day and repeat the procedure with the next cell in water_cells.
The slides below help to understand the solution in a better way.
1 of 19
2 of 19
3 of 19
4 of 19
5 of 19
6 of 19
7 of 19
8 of 19
9 of 19
10 of 19
11 of 19
12 of 19
13 of 19
14 of 19
15 of 19
16 of 19
17 of 19
18 of 19
19 of 19
Let’s take a look at the code for this solution below:
Time complexity#
The time complexity of this solution is , where represents the number of rows in the matrix, represents the number of columns in the matrix, and is the inverse Ackermann function that grows very slowly and whose maximum value for all practical purposes is .
The initialization of the UnionFind object takes time, since it involves creating an array of size and initializing every element to a unique index.
There may be, in the worst case, entries in water_cells. For each entry, there can be a maximum of eight union operations to merge the flooded cell with adjacent water cells, and each union operation takes time on average.
Therefore, the overall time complexity of this solution is .
Space complexity#
The space complexity of the solution above is .
Alternative solutions#
Now, let’s see what are some other ways to solve this problem.
-
Union find on land cells:
This is similar to the solution presented above. Here, we start at the ending state of
matrixwith all the cells flooded. Then, moving from the end of thewater_cellsarray towards its start, we start rolling back the flooding (one cell at a time), that is, changing that cell’s value from to . After changing a cell from water to land, we check the land cells adjacent to it in all four cardinal directions, and if any are found, we connect them to the recently reverted cell. To check whether we got a single connected component of land cells from top to bottom ofmatrix, we keep two virtual nodes—one at the top and the other at the bottom. The moment we find these virtual nodes connected to each other, we’ll return this day as the output.The time and space complexity of this solution is the same as the solution above.
-
Breadth-first search (BFS) with binary search:
In this solution, for each new flooded cell on the day, we use BFS to figure out whether we have a path of land cells from top to bottom to cross
matrix. There is a total of cells that needs to be flooded as perwater_cells, and finding a path from the top to the bottom row for each of them would be highly inefficient. Therefore, we use binary search to reduce this number. Our binary search method is guided by these two observations:- If we can cross on day , we can cross on any day before that day.
- If we can’t cross on day , we cannot cross on any day after that day.
For example, if there are cells in
water_cells, then, instead of searching for paths on each of the days, we start from the middle cell, the cell. We flood all the cells mentioned inwater_cellsup to the day and then check if a path to crossmatrixexists. If it does, then we flood all the cells mentioned up to the day, and check again. If no path exists, we roll back the flooding to the day and check again, and so on.To find a continuous path from top to bottom to cross
matrix, we start the BFS from the top row ofmatrixand insert all its land cells into a queue. Then, we start to dequeue the elements in the queue one by one until it’s empty. Whenever we dequeue a cell, we check all of its adjacent land cells, and if we have not visited them yet, we add them to the queue. After checking all of its adjacent land cells, we mark the cell as visited and process the next element in the queue. We repeat this until we reach a cell that is in the bottom row ofmatrix. At this point, we have found a path from the top row to the bottom row. Another possible terminating condition for the BFS is that we are unable to find a path from any of the cells in the top row that reaches a cell in the bottom row.Overall, the binary search takes , and the BFS takes time, so the time complexity of this solution is . The space complexity of this solution is .
-
Depth-first search (DFS) with binary search:
This solution is similar to the one we just discussed, except it uses DFS instead of BFS. On each day, for each cell in the top row of
matrix, we explore its unvisited neighboring land cells recursively. We continue doing this until we reach the bottom row, or until all reachable land cells inmatrixhave been visited. To avoid revisiting the same cell, we just mark the cell as visited.The time and space complexity of this solution is the same as that of the BFS with binary search.
Last Day Where You Can Still Cross
Minimize Malware Spread