Problem overview
The function pacificAtlantic receives heights, a vector<vector<int>> where heights[r][c] is the height of the cell in row r and column c. Rows are numbered from 0 at the top and columns from 0 at the left. 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 cell on an edge into the ocean that touches that edge. The Pacific touches the top and left edges, and the Atlantic touches the bottom and right edges.
The function returns the cells from which water can reach both oceans, as a vector of {r, c} pairs in reading order: row by row from the top, and left to right within a row.
For the 5x5 grid in the first example the answer is [[0, 4], [1, 3], [1, 4], [2, 2], [3, 0], [3, 1], [4, 0]]. The cell [0, 4] sits on the top edge and the right edge. From [2, 2], height 5, water runs up to the Pacific and right to the Atlantic. For [[1]] the answer is [[0, 0]], since the only cell touches every edge. In the grid of 5s with a 1 in the middle, the 1 is lower than all its neighbours, so its water stays where it is, and the eight 5s are the answer because water passes between cells of equal height.
The lesson asks for O(m · n) time and O(m · n) extra space, for m rows and n columns.
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 each cell's search wanders over the whole lake before giving up.
Turn the question around. Instead of asking where a cell's water goes, ask which cells can send water to a given ocean. Start from the cells along that ocean's edges, since their water flows straight in. From a cell, move to a neighbour only when the neighbour is at least as high: water from there can flow down (or stay level) into the cell we came from, and from there on to the ocean. Every cell the search reaches can send its water to that ocean.
That is a BFS that starts from many cells at once, all the edge cells of one ocean, in the same queue. A seen grid marks the cells already reached, so each cell is queued at most once. Run it for the Pacific and again for the Atlantic. The answer is the cells that both searches reached. A single helper, reach, serves both oceans, because only the edges differ: the Pacific is row 0 and column 0, and the Atlantic is the last row and the last column.
The answers come out in reading order for free, because the final loop goes through the rows from the top and through the columns from the left.
Step by step
- Store the size.
rowsisheights.size()andcolsisheights[0].size(), both in ints. Every row has the same length. - Find the Pacific cells:
reach(heights, 0, 0)stored in avector<vector<bool>>namedpacific. The lesson usesreachbefore writing it: given an edge row and an edge column, it returns a grid marking every cell whose water can flow to them. - Find the Atlantic cells:
reach(heights, rows - 1, cols - 1)inatlantic. - Create
result, an emptyvector<vector<int>>. - Loop over the rows with
r, then over the columns withcinside it. - Collect the cells reached by both. When
pacific[r][c]andatlantic[r][c]are both true, push{r, c}ontoresult. - Return
result. - Write
reachas a method takingvector<vector<int>>& heightsand two ints,edgeRowandedgeCol, returning avector<vector<bool>>. - Store the size again in the helper's own
rowsandcols, because the variables of the other method aren't visible here. - Create
seenwithrowsrows, each avector<bool>ofcolsfalses. - Create the queue,
queue<pair<int, int>> q, where each element is a cell's row and column. - Seed the search. Loop over every cell. When
r == edgeRow || c == edgeCol, the cell touches the ocean: setseen[r][c]totrueand push{r, c}. A cell is marked when it joins the queue, so it never joins twice. - Define the four moves in a
vector<pair<int, int>>nameddirections:{-1, 0},{1, 0},{0, -1}and{0, 1}. Diagonal cells aren't neighbours. - Loop while the queue isn't empty. Unpack the front with
auto [row, col] = q.front()and then callq.pop(). - Loop over the moves, unpacking each into
dranddc, and computenextRow = row + drandnextCol = col + dc. - Skip positions off the grid: both coordinates must be at least 0 and below
rowsorcols. - Climb when allowed: the neighbour isn't seen and
heights[nextRow][nextCol] >= heights[row][col]. This is the key line. The search walks against the flow. - Mark and queue the neighbour. Its own neighbours are checked when it leaves the queue.
- Return
seenafter the loop.
The solution in C++
class Solution {
public:
vector<vector<int>> pacificAtlantic(vector<vector<int>>& heights) {
int rows = heights.size();
int cols = heights[0].size();
vector<vector<bool>> pacific = reach(heights, 0, 0);
vector<vector<bool>> atlantic = reach(heights, rows - 1, cols - 1);
vector<vector<int>> result;
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
if (pacific[r][c] && atlantic[r][c]) {
result.push_back({r, c});
}
}
}
return result;
}
vector<vector<bool>> reach(vector<vector<int>>& heights, int edgeRow, int edgeCol) {
int rows = heights.size();
int cols = heights[0].size();
vector<vector<bool>> seen(rows, vector<bool>(cols, false));
queue<pair<int, int>> q;
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
if (r == edgeRow || c == edgeCol) {
seen[r][c] = true;
q.push({r, c});
}
}
}
vector<pair<int, int>> directions = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
while (!q.empty()) {
auto [row, col] = q.front();
q.pop();
for (auto [dr, dc] : 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;
q.push({nextRow, nextCol});
}
}
}
}
return seen;
}
};
The C++ details worth a closer look:
seen(rows, vector<bool>(cols, false)). The second argument is the value every row starts as, so each row gets its own copy of it. The result isrowsindependent rows ofcolsfalses.- Structured bindings.
auto [row, col] = q.front()names the two members of the pair. It needs C++17. The binding copies the pair, so popping the queue on the next line is safe.popreturns nothing, so the front has to be read first. - Brace initialisation.
q.push({r, c})builds apair<int, int>in place, andresult.push_back({r, c})builds avector<int>with two elements. The same braces make different types, depending on what the container expects. - Passing by reference.
vector<vector<int>>& heightslets the helper read the grid without copying up to 40,000 numbers on each call. - Returning a large vector by value.
return seenmoves the local grid out; it isn't copied. vector<bool>is bit-packed. It works as a grid of booleans here, but its elements are proxies, so you can't take abool&to one.
Complexity
Time: O(m · n). Each BFS marks a cell when it joins the queue, so a cell is queued at most once and each queue pop looks at four neighbours. The seeding loop and the final loop also visit each cell once. Two searches and two passes are still a constant multiple of m · n.
Space: O(m · n). The two seen grids and the queue each hold up to m · n entries, and the result is at most that large.
Edge cases and common mistakes
- A single cell.
[[1]]gives[[0, 0]]: the cell is on the edge row and the edge column for both oceans. - A single row or column.
[[1, 2, 3]]gives all three cells,[[0, 0], [0, 1], [0, 2]], because every cell touches both the top and bottom edges. A column such as[[3], [2], [1]]gives[[0, 0], [1, 0], [2, 0]]for the same reason. - Equal heights. Water flows between equal cells, so the test is
>=, not>. With a strict comparison the 5s of the third example wouldn't pass water along the border. - Climbing the wrong way. The search must go to neighbours that are at least as high. Going to lower ones would mark the cells the ocean's water can't reach.
- Marking when popping. Marking a cell only when it leaves the queue lets it be queued many times. Mark it when it joins.
- Row and column order. The answer pairs are
{r, c}, row first. Swapping them gives wrong cells on non-square grids, such as[[1, 2], [4, 3]], whose answer is[[0, 1], [1, 0], [1, 1]]. - Forgetting the bounds check. Cells on the edges have neighbours outside the grid, and reading them is undefined behaviour in C++, not an exception.
Testing
- The first example's 5x5 grid 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, 2], [4, 3]]gives[[0, 1], [1, 0], [1, 1]].
