Problem overview
A rectangular island is a grid of square cells with m rows and n columns. The list heights holds one list per row, and heights[r][c] is the height of the cell in row r and column c. The Pacific Ocean touches the top and left edges. The Atlantic Ocean touches the bottom and right edges.
Rain falls on every cell. Water flows from a cell to a neighbor (up, down, left or right) when the neighbor's height is less than or equal to the cell's height, and from any edge cell into the ocean that touches that edge. The method pacificAtlantic returns the cells whose water can reach both oceans, as [r, c] pairs in reading order: row by row from the top, left to right within a row.
For heights = [[1, 2, 2, 3, 5], [3, 2, 3, 4, 4], [2, 4, 5, 3, 1], [6, 7, 1, 4, 5], [5, 1, 1, 2, 4]] the answer is [[0, 4], [1, 3], [1, 4], [2, 2], [3, 0], [3, 1], [4, 0]]. The cell [0, 4] touches the top edge and the right edge, so it reaches both oceans directly. The cell [2, 2], with height 5, reaches the Pacific through [1, 2] and [0, 2], and the Atlantic through [2, 3] and [2, 4]. The cell [2, 1], with height 4, reaches the Pacific through [2, 0], but every path to the Atlantic needs a step up.
For [[1]] the answer is [[0, 0]], since the one cell touches all four edges. For a ring of 5s around a 1, all the 5s qualify, because water moves between equal heights, and the 1 in the middle does not: it is lower than all its neighbors.
The statement asks for O(m · n) time and O(m · n) extra space.
Approach: climb up from the oceans
The direct idea follows the water down from every cell, with one search per cell. That can cost O((m · n)²) in total: on a large flat lake, each cell's search wanders over the whole lake before it gives up.
Turn the question around. Instead of asking where a cell's water goes, ask which cells can send water to the Pacific. Start at the cells along the Pacific's edges, which reach it directly, and walk against the flow: move to a neighbor only when it is at least as high as the current cell. Water from that neighbor can flow down, or level, into the current cell, and from there to the ocean along the path already found. Every cell this search reaches can send its water to the Pacific.
A second search does the same from the Atlantic's edges. The answer is the cells that both searches reached. Each search is a breadth-first search that starts from all its edge cells at once and visits every cell at most once.
For the first example, the two searches mark these cells (# means reached):
pacific atlantic
##### ....#
##### ...##
###.. ..###
##... #####
#.... #####
The cells marked in both grids are exactly the seven in the answer: [0, 4], [1, 3], [1, 4], [2, 2], [3, 0], [3, 1] and [4, 0].
Step by step
The main method comes first, and then the helper reach that both oceans share.
- Store the grid's size:
rows = len(heights)andcols = len(heights[0]). Every row has the same length. - Find the Pacific cells with
self.reach(heights, 0, 0). The Pacific touches row 0 and column 0. The helper returns a grid of booleans, which the lesson writes after this method. - Find the Atlantic cells with
self.reach(heights, rows - 1, cols - 1). Only the edges differ. - Create an empty
resultlist. - Loop over
rinrange(rows), and inside it overcinrange(cols). Row by row and left to right is exactly the order the answer needs. - If
pacific[r][c]andatlantic[r][c]are both true, append[r, c]toresult. - Return
result. - Write
reach, a method takingself,heights,edgeRowandedgeCol. It returns a grid of booleans marking the cells whose water can reach the ocean at that row and column. - Store
rowsandcolsagain. The variables of the main method aren't visible here. - Create
seenas a grid ofFalsevalues with a list comprehension.seen[r][c]becomesTrueonce the search has reached the cell. - Create an empty
dequenamedqueue. It holds the cells that were reached but not yet looked around from, as(row, column)tuples. - Loop over every cell and, when
r == edgeRow or c == edgeCol, the cell touches the ocean. Setseen[r][c]toTrueand append(r, c)toqueue. A cell is marked when it joins the queue, so no cell joins twice. - Create
directions, the four moves as tuples:(-1, 0),(1, 0),(0, -1)and(0, 1). Diagonal cells aren't neighbors. - Loop while
queueis not empty. Each cell joins the queue at most once, so the loop runs at mostm · ntimes. - Take the front cell with
popleft()and unpack it intorowandcol. - Loop over
directionsasdr, dc, and computenextRow = row + drandnextCol = col + dc. - Check that the neighbor is inside the grid:
0 <= nextRow < rows and 0 <= nextCol < cols. - Check that the search can climb there: the neighbor isn't seen yet, and
heights[nextRow][nextCol] >= heights[row][col]. This is the key line: it walks against the flow. - Mark the neighbor as seen and append
(nextRow, nextCol)toqueue. - Return
seenafter the loop.
The solution in Python
class Solution:
def pacificAtlantic(self, heights: List[List[int]]) -> List[List[int]]:
rows = len(heights)
cols = len(heights[0])
pacific = self.reach(heights, 0, 0)
atlantic = self.reach(heights, rows - 1, cols - 1)
result = []
for r in range(rows):
for c in range(cols):
if pacific[r][c] and atlantic[r][c]:
result.append([r, c])
return result
def reach(self, heights: List[List[int]], edgeRow: int, edgeCol: int) -> List[List[bool]]:
rows = len(heights)
cols = len(heights[0])
seen = [[False] * cols for _ in range(rows)]
queue = deque()
for r in range(rows):
for c in range(cols):
if r == edgeRow or c == edgeCol:
seen[r][c] = True
queue.append((r, c))
directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
while queue:
row, col = queue.popleft()
for dr, dc in directions:
nextRow = row + dr
nextCol = col + dc
if 0 <= nextRow < rows and 0 <= nextCol < cols:
if not seen[nextRow][nextCol] and heights[nextRow][nextCol] >= heights[row][col]:
seen[nextRow][nextCol] = True
queue.append((nextRow, nextCol))
return seen
Python details worth a closer look:
- Building the grid.
[[False] * cols for _ in range(rows)]makes a separate row list for each row.[[False] * cols] * rowswould repeat one row object: afters = [[False] * 3] * 2ands[0][1] = True,sis[[False, True, False], [False, True, False]], and marking one cell marks a whole column. With the comprehension, the same assignment leaves[[False, True, False], [False, False, False]]. - The chained comparison
0 <= nextRow < rowschecks both bounds at once. The check matters because a negative index doesn't fail in Python.l[-1]reads the last element, so an uncheckednextRowof -1 would silently read the bottom row. - Tuples in the queue.
(r, c)is a tuple, androw, col = queue.popleft()unpacks it into two variables. The same unpacking reads each move infor dr, dc in directions. deque.popleft()takes constant time, unlikelist.pop(0). The lesson's editor already providesdeque. In a file of your own, addfrom collections import deque.- Lists of booleans can be used directly in
if.pacific[r][c] and atlantic[r][c]needs no== True.
Complexity
Time: O(m · n). Each of the two searches marks a cell once and queues it once, and it looks at four neighbors per cell, which is a constant. Building the result reads each cell once more.
Space: O(m · n). Each seen grid has m · n entries, and the queue holds at most that many cells.
Edge cases and common mistakes
- The wrong direction of the comparison. The search climbs, so it moves to a neighbor that is at least as high. Using
<=would follow the water instead, and from the edge cells it would find the wrong cells. - Strict instead of non-strict. With
>instead of>=, level paths are cut. For a ring of 5s around a 1, the answer would shrink to[[0, 2], [2, 0]]. - Only the corners. Every cell on an edge row or column touches the ocean, not only the corner cell. That is why the start loop tests
r == edgeRow or c == edgeCol. - A one-row or one-column grid.
[[1, 2, 3]]gives[[0, 0], [0, 1], [0, 2]]: every cell touches both oceans. A single column[[3], [2], [1]]works the same way. - A shared row object in the grid. It makes the marks leak between rows, as shown above.
- Reading order. The answer is built by scanning rows and columns, so it is in reading order without any sorting.
Testing
- The 5 by 5 grid above gives
[[0, 4], [1, 3], [1, 4], [2, 2], [3, 0], [3, 1], [4, 0]]. heights = [[1]]gives[[0, 0]].heights = [[5, 5, 5], [5, 1, 5], [5, 5, 5]]gives[[0, 0], [0, 1], [0, 2], [1, 0], [1, 2], [2, 0], [2, 1], [2, 2]].heights = [[1, 2, 3]]gives[[0, 0], [0, 1], [0, 2]].heights = [[3], [2], [1]]gives[[0, 0], [1, 0], [2, 0]].heights = [[1, 2], [4, 3]]gives[[0, 1], [1, 0], [1, 1]].
