Problem overview
PacificAtlantic receives heights, an int[][] grid of cell heights. 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 neighbor (up, down, left or right) 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 returns the cells from which water can reach both oceans, as a List<List<int>> of [r, c] pairs in reading order: row by row from the top, left to right within each row.
In the 5x5 example, [0, 4] is on the top edge (Pacific) and the right edge (Atlantic), so it qualifies at once. The cell [2, 1], height 4, reaches the Pacific through [2, 0], but every path to the Atlantic needs a step up, so it does not. In the 3x3 grid of 5s with a 1 in the middle, the 5s pass water along the border to both oceans because water may flow between equal heights, while the 1 is lower than all its neighbors and keeps its water.
Approach: climb up from the oceans
Following the water down from every cell costs one search per cell, up to O((m * n)^2) in total: in a large flat lake, each cell's search wanders over the whole lake before giving up.
Turn the question around. Start from the ocean and move against the flow. A breadth-first search begins from all the cells along the Pacific's edges at once, and moves to a neighbor only when the neighbor is at least as high, because water from that neighbor can flow down into the current cell and onward. Every cell it reaches can send water to the Pacific. A second search does the same from the Atlantic's edges. The answer is the cells both searches reach, and each search visits every cell at most once.
For the 5x5 example, the Pacific search marks these cells (P), and the Atlantic search marks these (A):
PPPPP ....A
PPPPP ...AA
PPP.. ..AAA
PP... AAAAA
P.... AAAAA
The cells marked in both grids are [0, 4], [1, 3], [1, 4], [2, 2], [3, 0], [3, 1] and [4, 0].
Step by step
The lesson writes the main method first, assuming a helper Reach, then the helper.
- Store the size.
rowsisheights.Length, andcolsisheights[0].Length, since every row has the same length. - Find the Pacific cells:
Reach(heights, 0, 0)into abool[,]namedpacific. The Pacific touches row 0 and column 0. - Find the Atlantic cells:
Reach(heights, rows - 1, cols - 1)intoatlantic. One helper serves both oceans; only the edges differ. - Create
result, an emptyList<List<int>>. - Loop over rows, then columns. Scanning row by row and left to right gives exactly the required order.
- Collect the cells. If
pacific[r, c]andatlantic[r, c]are both true, addnew List<int> { r, c }. Then returnresult. - Write
Reach. It takes the grid and an edge row and column, and returns abool[,], a two-dimensional array read asseen[r, c]. - Prepare the search. Recompute
rowsandcols(the helper cannot see the main method's variables), createseenasnew bool[rows, cols], and aQueue<(int, int)>whose elements are (row, column) tuples. - Seed the queue with the ocean's edges. For every cell with
r == edgeRow || c == edgeCol, setseen[r, c]totrueand enqueue(r, c). A cell is marked when it joins the queue, so none joins twice. - Define the four moves as an array
(int, int)[] directions:(-1, 0),(1, 0),(0, -1),(0, 1). Diagonal cells are not neighbors. - Loop while
queue.Count > 0. Take the front cell and deconstruct it:(int row, int col) = queue.Dequeue();. - Try each move, unpacking it into
dranddc, and computenextRowandnextCol. - Skip cells off the grid by checking both indexes against
0and the size. - Climb only against the flow. If the neighbor is not seen and
heights[nextRow][nextCol] >= heights[row][col], mark it and enqueue it. - Return
seen.
The solution in C#
class Solution {
public List<List<int>> PacificAtlantic(int[][] heights) {
int rows = heights.Length;
int cols = heights[0].Length;
bool[,] pacific = Reach(heights, 0, 0);
bool[,] atlantic = Reach(heights, rows - 1, cols - 1);
List<List<int>> result = new List<List<int>>();
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
if (pacific[r, c] && atlantic[r, c]) {
result.Add(new List<int> { r, c });
}
}
}
return result;
}
public bool[,] Reach(int[][] heights, int edgeRow, int edgeCol) {
int rows = heights.Length;
int cols = heights[0].Length;
bool[,] seen = new bool[rows, cols];
Queue<(int, int)> queue = new Queue<(int, int)>();
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
if (r == edgeRow || c == edgeCol) {
seen[r, c] = true;
queue.Enqueue((r, c));
}
}
}
(int, int)[] directions = { (-1, 0), (1, 0), (0, -1), (0, 1) };
while (queue.Count > 0) {
(int row, int col) = queue.Dequeue();
foreach ((int dr, int dc) in directions) {
int nextRow = row + dr;
int 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));
}
}
}
}
return seen;
}
}
The C# details worth knowing:
- Two array shapes. The input
int[][]is jagged, read asheights[r][c]. The resultbool[,]is rectangular, read asseen[r, c]. Mixing the two notations is a compile error. - Tuples.
(int, int)is a value tuple.(int row, int col) = queue.Dequeue();deconstructs it into two variables, andforeach ((int dr, int dc) in directions)does the same for each move. - Array initializer.
(int, int)[] directions = { (-1, 0), (1, 0), (0, -1), (0, 1) };needs nonewwhen it is the initializer of a declaration. - The index checks come first.
seenandheightswould throwIndexOutOfRangeExceptionfor an off-grid index, so the boundsifwraps the other one. - No recursion. The queue replaces the call stack, so a 200x200 grid cannot overflow it.
Complexity
Time: O(m * n). Each search marks a cell once, enqueues it once and looks at its four neighbors once. Building the answer is another m * n pass.
Space: O(m * n) for the two seen grids and the queue.
Edge cases and common mistakes
- One cell.
{{1}}returns[[0,0]]: the cell touches all four edges. - One row.
{{1,2,3,4}}returns all four cells, because every cell of a single row touches the top and the bottom edge. - One column.
{{4},{3},{2}}returns all three cells for the same reason. - Equal heights. The comparison is
>=, not>, because water crosses between equal cells. With>the 3x3 example would lose its border cells. - Wrong direction. Moving to lower neighbors from the ocean edge would follow the flow's direction in reverse. The search climbs, since water from a higher neighbor can flow down into the cell.
- Marking when dequeuing. Marking a cell only when it leaves the queue lets it be enqueued several times.
- Order of the answer. Collecting cells while searching gives the wrong order. The final double loop gives reading order.
Testing
- The 5x5 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}}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,4}}gives[[0,0], [0,1], [0,2], [0,3]].{{4},{3},{2}}gives[[0,0], [1,0], [2,0]].
