The grid can be considered as an undirected graph where land cells are the vertices and an edge exists between two vertices if the land cells are neighbors.
DFS
In the grid graph, for each day, we can check if there is a path from any of the sources (land cells on first row) to any of the destinations (land cells on last row).
Time: , space:
.
class Solution:
def neibors(self, u, m, n, flooded_cells):
r, c = u
for dr, dc in [(-1, 0), (0, 1), (1, 0), (0, -1)]:
r2, c2 = r + dr, c + dc
if not (1 <= r2 <= m):
continue
if not (1 <= c2 <= n):
continue
if (r2, c2) in flooded_cells:
continue
yield (r2, c2)
def has_path(self, u, destinations, m, n, flooded_cells, visited):
if u in destinations:
return True
visited.add(u)
for v in self.neibors(u, m, n, flooded_cells):
if v in visited:
continue
if self.has_path(v, destinations, m, n, flooded_cells, visited):
return True
visited.remove(u)
return False
def latestDayToCross(self, row: int, col: int, cells: List[List[int]]) -> int:
sources, destinations = set(), set()
for col in range(1, col + 1):
sources.add((1, col))
destinations.add((row, col))
flooded_cells = set()
for day, cell in enumerate(cells, start=1):
u = tuple(cell)
flooded_cells.add(u)
sources.discard(u)
destinations.discard(u)
if not any(
self.has_path(s, destinations, row, col, flooded_cells, set())
for s in sources
):
return day - 1
return None
BFS
Time: , space:
.
class Solution:
def neibors(self, u, m, n, flooded_cells):
r, c = u
for dr, dc in [(-1, 0), (0, 1), (1, 0), (0, -1)]:
r2, c2 = r+dr, c+dc
if not (1 <= r2 <= m):
continue
if not (1 <= c2 <= n):
continue
if (r2, c2) in flooded_cells:
continue
yield (r2, c2)
def has_path(self, sources, destinations, m, n, flooded_cells):
visited = set(sources)
q = deque(list(sources))
while q:
u = q.popleft()
if u in destinations:
return True
for v in self.neibors(u, m, n, flooded_cells):
if v in visited:
continue
visited.add(v)
q.append(v)
return False
def latestDayToCross(self, row: int, col: int, cells: List[List[int]]) -> int:
sources, destinations = set(), set()
for col in range(1, col+1):
sources.add((1, col))
destinations.add((row, col))
flooded_cells = set()
for day, cell in enumerate(cells, start=1):
u = tuple(cell)
flooded_cells.add(u)
sources.discard(u)
destinations.discard(u)
if not self.has_path(sources, destinations, row, col, flooded_cells):
return day-1
return None
BFS with heuristic
We can use Manhattan distance to one of the destinations as a heuristic to direct the path finding towards destination.
from heapq import heapify, heappush as push, heappop as pop
class Solution:
def neibors(self, u, m, n, flooded_cells):
r, c = u
for dr, dc in [(-1, 0), (0, 1), (1, 0), (0, -1)]:
r2, c2 = r+dr, c+dc
if not (1 <= r2 <= m):
continue
if not (1 <= c2 <= n):
continue
if (r2, c2) in flooded_cells:
continue
yield (r2, c2)
def has_path(self, sources, destinations, m, n, flooded_cells):
def manhattan_distance(u, v):
return abs(u[0]-v[0]) + abs(u[1]-v[1])
if not destinations:
return False
visited = set(sources)
d0 = next(d for d in destinations)
q = []
for s in sources:
dist = manhattan_distance(s, d0)
q.append((dist, 0, s))
heapify(q)
while q:
_, l, u = pop(q)
if u in destinations:
return True
for v in self.neibors(u, m, n, flooded_cells):
if v in visited:
continue
visited.add(v)
dist = manhattan_distance(v, d0)
push(q, (l+dist, l+1, v))
return False
def latestDayToCross(self, row: int, col: int, cells: List[List[int]]) -> int:
sources, destinations = set(), set()
for col in range(1, col+1):
sources.add((1, col))
destinations.add((row, col))
flooded_cells = set()
for day, cell in enumerate(cells, start=1):
u = tuple(cell)
flooded_cells.add(u)
sources.discard(u)
destinations.discard(u)
if not self.has_path(sources, destinations, row, col, flooded_cells):
return day-1
return None
Union-Find
In the beginning the grid (land) graph has a single, big connected component. As the cells get flooded, the number of connected components increases or the size of connected components shrink. The latest day when a connected component still has cells from both the first row and the last row, is the day we want. For the example below, we want day 3.

We can also think backwards, starting from the last flood day. Initially the land graph is empty and as we undo flooded cells, the connected component count or size grows. For the example above, Day 4 is then the first day which if we undid, would have had a connected component having cells from both row 1 and row 3. So, the day before Day 4, is the day we want. We can use Union-Find to keep track of the connected components — represented as row-set. During union, we merge the two row sets.
Time: , space:
.
class UnionFind:
def __init__(self, m, n):
self.m, self.n = m, n
self.parent_of, self.rank_of = {}, {}
self.cc = {}
self.top_bottom_connected = False
def neibors(self, u):
r, c = u
for dr, dc in [(-1, 0), (0, 1), (1, 0), (0, -1)]:
r2, c2 = r + dr, c + dc
if not (1 <= r2 <= self.m):
continue
if not (1 <= c2 <= self.n):
continue
if (r2, c2) not in self.parent_of:
continue
yield (r2, c2)
def __compress_path(self, u, root):
while u != (p := self.parent_of[u]):
self.parent_of[u] = root
u = p
def __find(self, u):
root = u
while root != (p := self.parent_of[root]):
root = p
self.__compress_path(u, root)
return root
def __union(self, u, v):
def update_cc(root, child):
self.cc[root].update(self.cc[child])
del self.cc[child]
self.top_bottom_connected = 1 in self.cc[root] and self.m in self.cc[root]
root_u = self.__find(u)
root_v = self.__find(v)
if root_u == root_v:
return
rank_u = self.rank_of[root_u]
rank_v = self.rank_of[root_v]
root, child = None, None
if rank_u == rank_v:
self.parent_of[root_u] = root_v
self.rank_of[root_v] += 1
update_cc(root_v, root_u)
return
if rank_u < rank_v:
self.parent_of[root_u] = root_v
update_cc(root_v, root_u)
else:
self.parent_of[root_v] = root_u
update_cc(root_u, root_v)
def add(self, u):
self.parent_of[u] = u
self.rank_of[u] = 0
self.cc[u] = {u[0]}
for v in self.neibors(u):
self.__union(u, v)
def has_path(self):
return self.top_bottom_connected
class Solution:
def latestDayToCross(self, row: int, col: int, cells: List[List[int]]) -> int:
uf = UnionFind(row, col)
for d in range(len(cells), 0, -1):
u = tuple(cells[d - 1])
uf.add(u)
if uf.has_path():
# If we undid day d, we would have
# had a path. So, the previous day is
# the last day the top and bottom
# were connected.
return d - 1
return None
Binary search with BFS
Note, has_path() is a step-function — once it evaluates to False, it remains False. In other words, has_path() is a monotonic function of days. So, we can use binary-search on the days to find the last day where has_path() is True.
Time: , space:
.
class Solution:
def neibors(self, u, m, n, flooded_cells):
r, c = u
for dr, dc in [(-1, 0), (0, 1), (1, 0), (0, -1)]:
r2, c2 = r+dr, c+dc
if not (1 <= r2 <= m):
continue
if not (1 <= c2 <= n):
continue
if (r2, c2) in flooded_cells:
continue
yield (r2, c2)
def has_path(self, m, n, cells, day):
sources, destinations = set(), set()
for col in range(1, n+1):
sources.add((1, col))
destinations.add((m, col))
flooded_cells = set()
for d in range(day):
u = tuple(cells[d])
flooded_cells.add(u)
sources.discard(u)
destinations.discard(u)
visited = set(sources)
q = deque(list(sources))
while q:
u = q.popleft()
if u in destinations:
return True
for v in self.neibors(u, m, n, flooded_cells):
if v in visited:
continue
visited.add(v)
q.append(v)
return False
def latestDayToCross(self, row: int, col: int, cells: List[List[int]]) -> int:
lo, hi = 1, len(cells)
while lo <= hi:
mid = (lo+hi) // 2
if self.has_path(row, col, cells, mid):
lo = mid+1
else:
hi = mid-1
return lo-1
Leave a comment