Problem overview
A rectangular island sits between two oceans. The Pacific touches its left and top edges, and the Atlantic touches its right and bottom edges. The island is a grid heights, an Array<IntArray> where heights[r][c] is the height of the cell in row r and column c. Rain falls on every cell. Water flows from a cell to a neighbor above, below, left or right when the neighbor's height is less than or equal to the cell's, and from any edge cell into the ocean that touches that edge.
pacificAtlantic returns the cells from which water can reach both oceans, as a list of [r, c] pairs in reading order: row by row from the top, and left to right within a row.
For the 5 by 5 grid in the first example the result is [[0, 4], [1, 3], [1, 4], [2, 2], [3, 0], [3, 1], [4, 0]]. For [[1]] it is [[0, 0]], because the single cell touches all four edges. For a ring of 5s around a single 1, every cell except the middle one qualifies: water passes along equal heights, and the 1 is lower than all its neighbors, so its water stays where it is.
Approach
The direct way follows the water down from each cell with a search of its own. That can cost O((m · n)²): on a large flat lake in the middle of the island, each cell's search wanders over the whole lake before giving up.
Turning the question around fixes this. Instead of asking where a cell's water goes, ask which cells can send water to the Pacific, and start from the Pacific. A breadth-first search begins at all cells along the Pacific's two edges at once and moves to a neighbor only when the neighbor is at least as high as the current cell, because water from that neighbor can then flow down to the current cell and on to the ocean. Every cell the search reaches can reach the Pacific. A second search does the same from the Atlantic's edges. The answer is the cells that both searches reach. Each search visits every cell at most once, so the total is O(m · n).
The two grids the searches produce for the first example, from a real run (1 means the cell's water reaches that ocean):
pacific: atlantic:
1 1 1 1 1 0 0 0 0 1
1 1 1 1 1 0 0 0 1 1
1 1 1 0 0 0 0 1 1 1
1 1 0 0 0 1 1 1 1 1
1 0 0 0 0 1 1 1 1 1
The cells that hold a 1 in both grids are the seven in the answer, such as [2, 2], whose height is 5.
Step by step
The main function pacificAtlantic does the following:
- Store the size.
rows = heights.sizeandcols = heights[0].size. Every row has the same length, so the first row gives the number of columns. - Find the Pacific cells.
reach(heights, 0, 0)returns a boolean grid. The Pacific touches row 0 and column 0. - Find the Atlantic cells.
reach(heights, rows - 1, cols - 1). The Atlantic touches the last row and the last column. One helper serves both oceans; only the edges differ. - Collect the answer.
resultis amutableListOf<List<Int>>(). Two nested loops walk the grid row by row, left to right, which is the required order. A cell is added aslistOf(r, c)when both grids hold true for it. - Return
result.
The helper reach(heights, edgeRow, edgeCol) is the BFS:
- Create
seen.Array(rows) { BooleanArray(cols) }gives every row its own array, all false. A cell is marked when it joins the queue, so it never joins twice. - Create the queue.
ArrayDeque<Pair<Int, Int>>(); each element is a cell, aPairof row and column. - Seed it with the ocean's edge cells. Loop over every cell and test
r == edgeRow || c == edgeCol. Those cells touch the ocean, so mark them and addPair(r, c). - List the four moves.
arrayOf(Pair(-1, 0), Pair(1, 0), Pair(0, -1), Pair(0, 1)): up, down, left and right. Diagonal cells are not neighbors. - Loop while the queue is not empty. Take the front cell with
removeFirst()and unpack it:val (row, col) = .... - Try each move.
for ((dr, dc) in directions), withnextRow = row + drandnextCol = col + dc. - Stay inside the grid. Check that both new indexes are at least 0 and below
rowsorcols, before using them as indexes. - Climb when allowed. If the neighbor is not seen and
heights[nextRow][nextCol] >= heights[row][col], mark it and add it to the queue. This is the key line: the search walks against the flow. - Return
seen.
The solution in Kotlin
class Solution {
fun pacificAtlantic(heights: Array<IntArray>): List<List<Int>> {
val rows = heights.size
val cols = heights[0].size
val pacific = reach(heights, 0, 0)
val atlantic = reach(heights, rows - 1, cols - 1)
val result = mutableListOf<List<Int>>()
for (r in 0 until rows) {
for (c in 0 until cols) {
if (pacific[r][c] && atlantic[r][c]) {
result.add(listOf(r, c))
}
}
}
return result
}
fun reach(heights: Array<IntArray>, edgeRow: Int, edgeCol: Int): Array<BooleanArray> {
val rows = heights.size
val cols = heights[0].size
val seen = Array(rows) { BooleanArray(cols) }
val queue = ArrayDeque<Pair<Int, Int>>()
for (r in 0 until rows) {
for (c in 0 until cols) {
if (r == edgeRow || c == edgeCol) {
seen[r][c] = true
queue.add(Pair(r, c))
}
}
}
val directions = arrayOf(Pair(-1, 0), Pair(1, 0), Pair(0, -1), Pair(0, 1))
while (queue.isNotEmpty()) {
val (row, col) = queue.removeFirst()
for ((dr, dc) in directions) {
val nextRow = row + dr
val 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.add(Pair(nextRow, nextCol))
}
}
}
}
return seen
}
}
Kotlin details worth knowing:
Array(rows) { BooleanArray(cols) }runs the block once per row, so each row is a separate array. Repeating one array object would make all the rows share the same cells.- Destructuring. A
Pairsupportsval (row, col) = pair, which names its two parts. The same works in the loop header:for ((dr, dc) in directions). - Return types. The function returns
List<List<Int>>. ThemutableListOflist is returned as a read-onlyList, andlistOf(r, c)builds each pair. - Short-circuit checks. In the bounds test,
&&stops at the first false condition. The height comparison sits in a secondif, which only runs once the indexes are known to be valid. ArrayDequeis Kotlin's own, withaddat the back andremoveFirstat the front.
Complexity
Each call of reach marks a cell when it joins the queue, so a cell is queued at most once and examined with four moves: O(m · n) time. The function calls it twice and then walks the grid once, which stays O(m · n). The two seen grids, the queue and the result take O(m · n) extra space.
Edge cases and common mistakes
- Climbing with
>instead of>=. Water also flows between cells of equal height. With a strict comparison the ring of 5s would not be passed along, and the answer would lose cells. - Comparing in the wrong direction. The search starts at the ocean and goes uphill, so the neighbor must be at least as high as the current cell. Using
<=would follow the water downhill from the ocean, which answers a different question. - Seeding only one edge. The Pacific is the top row and the left column; skipping one of them loses cells that reach the ocean only through that edge.
- Marking cells when they leave the queue. A cell could then be queued several times. Marking as it joins keeps each cell to one entry.
- Single cell.
[[1]]gives[[0, 0]]: the cell is on every edge. - Single row or column. Every cell touches one ocean through its row or column edge, and often both.
[[1, 2, 3, 4]]gives all four cells, and so does the column[[4], [3], [2]]with its three cells. - A low cell surrounded by high ones. The 1 in the middle of the ring of 5s is not in the result: no search ever climbs to it from a lower cell.
- Order of the result. Collecting the cells with the nested loops gives reading order with no sorting needed.
Testing
Results from running the solution:
- The 5 by 5 grid of the first example:
[[0, 4], [1, 3], [1, 4], [2, 2], [3, 0], [3, 1], [4, 0]] [[1]]:[[0, 0]][[5, 5, 5], [5, 1, 5], [5, 5, 5]]:[[0, 0], [0, 1], [0, 2], [1, 0], [1, 2], [2, 0], [2, 1], [2, 2]][[1, 2, 3, 4]]:[[0, 0], [0, 1], [0, 2], [0, 3]][[4], [3], [2]]:[[0, 0], [1, 0], [2, 0]][[1, 2], [4, 3]]:[[0, 1], [1, 0], [1, 1]]- A 200 by 200 grid where every height is 7: 40,000 cells, returned quickly
