Problem overview
An island is a grid of cells, given as heights, an array of rows of integers. The Pacific Ocean touches the left and top edges, and the Atlantic Ocean touches the right and bottom edges. Rain falls on every cell. Water flows from a cell to an up, down, left or right neighbor whose height is less than or equal to the cell's, and from any edge cell into the ocean that touches that edge.
The method pacificAtlantic returns the cells from which 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 the grid
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 both the top edge and the right edge. From [2, 2], with height 5, water runs up to the Pacific through [1, 2] and [0, 2], and right to the Atlantic through [2, 3] and [2, 4]. A single cell, [[1]], touches all four edges and returns [[0, 0]].
The statement asks for O(m · n) time and O(m · n) extra space, for m rows and n columns.
Approach: climb up from each ocean
The direct idea follows the water down from every cell, one search per cell. That can cost O((m · n)²): on a big flat lake in the middle of the island, every cell's search wanders over the whole lake before it gives up.
Turn the question around. Start at an ocean and climb. A cell at the edge sends its water straight into the ocean. A neighbor can send its water to the ocean through that cell when the neighbor is at least as high, because water moves down or level. So a breadth-first search that starts from all the edge cells of one ocean at once, and moves to a neighbor only when it is at least as high, reaches exactly the cells whose water can flow to that ocean.
Run it twice: once from the top row and left column (Pacific), once from the bottom row and right column (Atlantic). A cell belongs to the answer when both searches reached it. Each search visits every cell at most once.
For the grid above, the Pacific search marks these cells (#) and the Atlantic search marks these:
pacific atlantic
# # # # # . . . . #
# # # # # . . . # #
# # # . . . . # # #
# # . . . # # # # #
# . . . . # # # # #
The cells marked in both are the seven in the answer.
Step by step
The lesson writes the main method first, assuming a helper reach, then the helper.
- Store the size:
rowsisheights.countandcolsisheights[0].count. Every row has the same length. - Find the Pacific cells:
reach(heights, 0, 0)goes intopacific. The helper takes an edge row and an edge column. The Pacific touches row 0 and column 0. - Find the Atlantic cells:
reach(heights, rows - 1, cols - 1)goes intoatlantic. One helper serves both oceans. - Create
resultas an empty[[Int]]. - Loop
rover0..<rows. Row by row, then left to right, is the order the answer needs. - Loop
cover0..<cols. - If
pacific[r][c]andatlantic[r][c]are both true, append[r, c]. - Return
result. - Write
reachwith parameters_ heights: [[Int]],_ edgeRow: Int,_ edgeCol: Int, returning a[[Bool]]. - Store the size again. The variables in
pacificAtlanticaren't visible in the helper. - Create
seen,rowsarrays ofcolsfalsevalues. It becomestruefor every cell the search reaches. - Create
queueas an empty array of(Int, Int)tuples, each one a row and a column. - Loop over the rows,
- and over the columns. The search starts from every edge cell at once.
- Check
r == edgeRow || c == edgeCol. Such a cell touches the ocean. - Mark it and queue it:
seen[r][c] = trueand append(r, c). A cell is marked when it joins the queue, so none joins twice. - Define
directionsas(-1, 0),(1, 0),(0, -1),(0, 1): up, down, left, right. Diagonal cells aren't neighbors. - Create
headat 0, the index of the next queued cell. This avoidsremoveFirst(), which shifts every remaining element. - Loop while
head < queue.count. Each cell is queued at most once, so this runs at mostm · ntimes. - Unpack the tuple at
headwithlet (row, col) = queue[head]. - Add 1 to
head. - Loop over
directionsas(dr, dc). - Compute
nextRowandnextCol. - Check the neighbor is inside the grid before using it as an index.
- Check it can be climbed to: not seen yet, and
heights[nextRow][nextCol] >= heights[row][col]. This is the key line: the search walks against the flow. - Mark and queue the neighbor.
- Return
seen.
The solution in Swift
class Solution {
func pacificAtlantic(_ heights: [[Int]]) -> [[Int]] {
let rows = heights.count
let cols = heights[0].count
let pacific = reach(heights, 0, 0)
let atlantic = reach(heights, rows - 1, cols - 1)
var result = [[Int]]()
for r in 0..<rows {
for c in 0..<cols {
if pacific[r][c] && atlantic[r][c] {
result.append([r, c])
}
}
}
return result
}
func reach(_ heights: [[Int]], _ edgeRow: Int, _ edgeCol: Int) -> [[Bool]] {
let rows = heights.count
let cols = heights[0].count
var seen = [[Bool]](repeating: [Bool](repeating: false, count: cols), count: rows)
var queue = [(Int, Int)]()
for r in 0..<rows {
for c in 0..<cols {
if r == edgeRow || c == edgeCol {
seen[r][c] = true
queue.append((r, c))
}
}
}
let directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
var head = 0
while head < queue.count {
let (row, col) = queue[head]
head += 1
for (dr, dc) in directions {
let nextRow = row + dr
let nextCol = col + dc
if nextRow >= 0 && nextRow < rows && nextCol >= 0 && nextCol < cols {
if !seen[nextRow][nextCol] && heights[nextRow][nextCol] >= heights[row][col] {
seen[nextRow][nextCol] = true
queue.append((nextRow, nextCol))
}
}
}
}
return seen
}
}
The Swift details worth a closer look:
- Nested arrays are values.
[[Bool]](repeating: [Bool](repeating: false, count: cols), count: rows)repeats the same row value, but arrays are copied when changed, soseen[r][c] = truechanges only that cell. - Tuples.
(Int, Int)keeps a row and a column together.let (row, col) = queue[head]andfor (dr, dc) in directionsunpack tuples into separate constants. - Type inference.
[(-1, 0), (1, 0), (0, -1), (0, 1)]is inferred as[(Int, Int)], so no annotation is needed. - Short-circuit checks. In the bounds test,
&&stops at the firstfalse. Only after all four bounds hold does the nextifuse the indexes. An out-of-range index would trap and crash the program. - Methods calling methods.
reachbelongs to the same class, sopacificAtlanticcalls it by name.
Complexity
Time: O(m · n). Each search marks a cell once when it is queued, so it handles at most m · n cells, with four neighbor checks each. Two searches and one pass over the grid for the answer stay within O(m · n).
Space: O(m · n). Each search has a seen grid and a queue of up to m · n cells, and the two grids are kept together.
Edge cases and common mistakes
- Single cell.
[[1]]gives[[0, 0]], since the cell is on every edge. - Equal heights. Water flows between cells of equal height, so the comparison is
>=. In[[5, 5, 5], [5, 1, 5], [5, 5, 5]]all eight 5s are in the answer, and the 1 in the middle is not. With a strict>, only 2 of the 9 cells would pass. - Direction. The search climbs, so it moves to a neighbor that is at least as high, not at most as high. Reversing the comparison answers a different question.
- One row or one column.
[[1, 2, 3]]gives all three cells, because each is on both a Pacific and an Atlantic edge.[[3], [2], [1]]does the same. - Flat grids.
[[1, 1], [1, 1]]returns all four cells. - Result order. The answer is built by scanning rows then columns, which is already in reading order, so no sorting is needed.
- Marking late. Marking a cell when it leaves the queue instead of when it joins lets it be queued several times.
Testing
- The 5 by 5 grid above gives
[[0, 4], [1, 3], [1, 4], [2, 2], [3, 0], [3, 1], [4, 0]]. [[1]]gives[[0, 0]].[[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]].[[1, 2, 3]]gives[[0, 0], [0, 1], [0, 2]].[[3], [2], [1]]gives[[0, 0], [1, 0], [2, 0]].[[1, 1], [1, 1]]gives[[0, 0], [0, 1], [1, 0], [1, 1]].[[3, 2, 1], [2, 1, 0]]gives[[0, 0], [0, 1], [0, 2], [1, 0]].
