Problem overview
The method pacificAtlantic(heights: Array[Array[Int]]): ArrayBuffer[Array[Int]] works on a rectangular island given as a grid of heights, heights(r)(c) for row r and column c, counted from 0 at the top and the left. The Pacific Ocean touches the top and left edges, and the Atlantic touches the bottom and right edges.
Rain falls on every cell. Water flows from a cell to a neighbouring cell (up, down, left or right) when the neighbour's height is less than or equal to the cell's, and from any edge cell into the ocean that touches that edge. The method 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 inside a row.
In the first example, the grid is
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
and the answer is [[0, 4], [1, 3], [1, 4], [2, 2], [3, 0], [3, 1], [4, 0]]. For example, [0, 4] sits on the top edge, which is the Pacific, and on the right edge, which is the Atlantic. A single cell, Array(Array(1)), touches all four edges and gives [[0, 0]].
Approach: climb up from the oceans
The direct idea follows the water down from every cell, one search per cell. That can cost O((m · n)²): picture a large flat lake in the middle of the island, where every cell's search wanders over the whole lake before giving up.
The lesson turns the question around. Start from an ocean and climb. A breadth-first search begins at all the cells along the Pacific's edges at once and moves to a neighbour only when the neighbour is at least as high, because water from there can flow down to the cell we are on. Every cell the 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 reach, listed in reading order. Each search visits every cell at most once, so the total is O(m · n).
For the first example, the Pacific search marks these cells (T) and leaves the others unmarked (.):
T T T T T
T T T T T
T T T . .
T T . . .
T . . . .
And the Atlantic search marks:
. . . . T
. . . T T
. . T T T
T T T T T
T T T T T
The cells marked in both grids are exactly the seven listed above.
Step by step
- Store the grid size.
rowsisheights.lengthandcolsisheights(0).length. Every row has the same length, so the first row's length is the number of columns. - Find the cells that reach the Pacific with
reach(heights, 0, 0), stored inpacific. The Pacific touches row 0 and column 0. The helperreachis written after this method; it returns a grid of booleans marking every cell whose water can flow to the given edge row or edge column. - Do the same for the Atlantic with
reach(heights, rows - 1, cols - 1), stored inatlantic. One helper serves both oceans; only the edges differ. - Create
result, an emptyArrayBuffer[Array[Int]]. - Loop over rows, then columns, with
r <- 0 until rowsandc <- 0 until 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, appendArray(r, c)toresultwith+=. - Return
resultas the last expression. - Write
reach(heights, edgeRow, edgeCol)with anArray[Array[Boolean]]result. It has its ownrowsandcols, since the ones inpacificAtlanticare not visible there. - Create
seenwithArray.fill(rows, cols)(false):rowsrows ofcolsfalse values.seen(r)(c)becomestrueonce the search reaches that cell. - Create
queue, an emptymutable.Queue[(Int, Int)]. Each element is a cell, a tuple of its row and column. - Seed the queue. Loop over every cell, and where
r == edgeRow || c == edgeCol, setseen(r)(c)totrueand enqueue(r, c). A cell on the edge row or column touches the ocean, so its water flows straight in. A cell is marked when it joins the queue, never later, so none joins twice. - List the four moves in
directions, anArrayof (row change, column change) tuples:(-1, 0),(1, 0),(0, -1)and(0, 1). Diagonal cells are not neighbours. - Loop while
queue.nonEmpty. Take the front cell withdequeue()and unpack it intorowandcolwithval (row, col) = .... - Loop over the moves, unpacking each into
dranddc, and computenextRowandnextCol. - Skip positions off the grid. Cells on the edges have neighbours outside, which must not be used as indexes.
- Climb when allowed. If the neighbour is not seen yet and
heights(nextRow)(nextCol) >= heights(row)(col), mark it seen and enqueue it. This is the key line: the search walks against the flow. Water moves down or level, so a neighbour at least as high can send its water into the current cell, and from there to the ocean. - Return
seenafter the loop. Atruemarks exactly the cells whose water can reach this ocean.
The solution in Scala
class Solution {
def pacificAtlantic(heights: Array[Array[Int]]): ArrayBuffer[Array[Int]] = {
val rows = heights.length
val cols = heights(0).length
val pacific = reach(heights, 0, 0)
val atlantic = reach(heights, rows - 1, cols - 1)
val result = ArrayBuffer[Array[Int]]()
for (r <- 0 until rows) {
for (c <- 0 until cols) {
if (pacific(r)(c) && atlantic(r)(c)) {
result += Array(r, c)
}
}
}
result
}
def reach(heights: Array[Array[Int]], edgeRow: Int, edgeCol: Int): Array[Array[Boolean]] = {
val rows = heights.length
val cols = heights(0).length
val seen = Array.fill(rows, cols)(false)
val queue = mutable.Queue[(Int, Int)]()
for (r <- 0 until rows) {
for (c <- 0 until cols) {
if (r == edgeRow || c == edgeCol) {
seen(r)(c) = true
queue.enqueue((r, c))
}
}
}
val directions = Array((-1, 0), (1, 0), (0, -1), (0, 1))
while (queue.nonEmpty) {
val (row, col) = queue.dequeue()
for ((dr, dc) <- 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.enqueue((nextRow, nextCol))
}
}
}
}
seen
}
}
The Scala details worth a closer look:
- Two-dimensional arrays are arrays of arrays.
seen(r)(c)first takes rowr, then elementcof it. Writing works the same way:seen(r)(c) = true. Array.fill(rows, cols)(false)buildsrowsseparate rows ofcolselements. Changing one row does not touch the others.- Tuples.
(Int, Int)is a pair type,(r, c)creates one, andval (row, col) = queue.dequeue()andfor ((dr, dc) <- directions)take pairs apart in a pattern. enqueue((r, c))has two pairs of parentheses on purpose: the outer ones are the call, the inner ones create the tuple.mutableandArrayBuffercome with the lesson. In a file of your own, addimport scala.collection.mutableandimport scala.collection.mutable.ArrayBuffer.- Reading order is free. Because the final loops go row by row, the
ArrayBufferis already sorted as the statement requires. No sorting is needed.
Complexity
Let the grid have m rows and n columns.
Time: O(m · n). Each of the two searches marks a cell once, when it joins the queue, and looks at at most four neighbours when it leaves. The final loop looks at every cell once.
Space: O(m · n). The two seen grids have m · n entries each, and a queue holds at most m · n cells. The result holds at most as many.
Edge cases and common mistakes
- A single cell.
Array(Array(1))gives[[0, 0]]: the cell is on both of the edges that matter. - One row or one column.
Array(Array(1, 2, 3))gives[[0, 0], [0, 1], [0, 2]], every cell, because each touches the top edge and the bottom edge.Array(Array(3), Array(2), Array(1))gives[[0, 0], [1, 0], [2, 0]]. - Equal heights flow. A grid of four 1s,
Array(Array(1, 1), Array(1, 1)), gives all four cells. The condition is>=when climbing, not>. In the grid of 5s with a 1 in the middle, the 5s pass water along the border, and the 1 is left out: it is lower than every neighbour, so the search never climbs into it, and the answer is the eight border cells. - Climbing the wrong way. Moving to a neighbour that is at least as low follows the water down from the ocean, which answers a different question.
- Marking cells when they leave the queue. A cell could then be queued many times by different neighbours. Mark it when it joins.
- Swapping the oceans' edges. The Pacific is the top row and left column. The Atlantic is the bottom row and right column. Using the grid's rows for columns and vice versa breaks non-square grids.
- Checking bounds after indexing. Reading
seen(nextRow)(nextCol)for a position outside the grid throws anArrayIndexOutOfBoundsException, so the range check comes first.
Testing
- The 5 by 5 grid above gives
[[0, 4], [1, 3], [1, 4], [2, 2], [3, 0], [3, 1], [4, 0]]. Array(Array(1))gives[[0, 0]].Array(Array(5, 5, 5), Array(5, 1, 5), Array(5, 5, 5))gives[[0, 0], [0, 1], [0, 2], [1, 0], [1, 2], [2, 0], [2, 1], [2, 2]].Array(Array(1, 2, 3))gives[[0, 0], [0, 1], [0, 2]].Array(Array(3), Array(2), Array(1))gives[[0, 0], [1, 0], [2, 0]].Array(Array(1, 1), Array(1, 1))gives[[0, 0], [0, 1], [1, 0], [1, 1]].
