1765. Map of Highest Peak
Problem Description
You have a grid representing a map with land and water cells. The grid isWater
has dimensions m x n
where:
isWater[i][j] = 0
means cell(i, j)
is landisWater[i][j] = 1
means cell(i, j)
is water
Your task is to assign a height value to each cell following these rules:
- All heights must be non-negative integers
- Water cells must have height
0
- Adjacent cells (horizontally or vertically connected) can differ in height by at most
1
The goal is to maximize the highest peak possible on the map while satisfying all constraints.
The solution uses a multi-source BFS approach. It starts by identifying all water cells as sources with height 0
and adds them to a queue. Then it performs BFS to assign heights to land cells layer by layer. Each land cell gets assigned a height that is 1
more than the minimum height of its already-processed neighbors. This ensures the height difference constraint is satisfied while maximizing the overall heights.
The algorithm works by:
- Initializing all cells to
-1
(unvisited) - Setting all water cells to height
0
and adding them to the queue - Processing cells level by level using BFS
- For each cell, checking its 4 adjacent neighbors
- If a neighbor hasn't been visited, assigning it a height of current cell's height + 1
This approach guarantees that each land cell gets the maximum possible height while maintaining the constraint that adjacent cells differ by at most 1.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The grid can be viewed as a graph where each cell is a node and adjacent cells (up, down, left, right) are connected by edges. We need to traverse this graph structure.
Is it a tree?
- No: The grid structure is not a tree. It's a graph where cells can have multiple paths between them and cycles can exist.
Is the problem related to directed acyclic graphs?
- No: The grid is essentially an undirected graph where adjacency is bidirectional.
Is the problem related to shortest paths?
- Yes: We need to find the minimum distance from each land cell to the nearest water cell. The height of each land cell is essentially its shortest distance to water.
Is the graph Weighted?
- No: Each step between adjacent cells has the same "cost" of 1 (height difference of 1), making this an unweighted graph problem.
BFS
- Yes: For unweighted shortest path problems, BFS is the optimal choice.
Conclusion: The flowchart suggests using BFS (Breadth-First Search) for this problem. This makes perfect sense because:
- We need to find the shortest distance from each land cell to the nearest water cell
- The graph is unweighted (each step increases height by exactly 1)
- BFS naturally processes cells level by level, ensuring we assign the minimum possible height to each cell while maintaining the constraint that adjacent cells differ by at most 1
- Using multi-source BFS (starting from all water cells simultaneously) efficiently computes the shortest distance from each cell to any water source
Intuition
The key insight is recognizing that this problem is fundamentally about finding distances. Each land cell's height represents its minimum distance to the nearest water source.
Think of water spreading outward from all water cells simultaneously. The water "rises" by 1 unit with each step it takes away from the source. This naturally creates the height map we're looking for - cells closer to water have lower heights, and cells farther away have higher heights.
Why does this work? Consider the constraints:
- Water cells must have height
0
(our starting points) - Adjacent cells can differ by at most
1
in height - We want to maximize heights
If we imagine pouring water that rises level by level, the first ring of land cells around water would get height 1
, the next ring would get height 2
, and so on. This ensures:
- We satisfy the adjacency constraint (each level differs by exactly 1)
- We maximize heights (each cell gets the highest possible value given its distance to water)
The multi-source BFS approach naturally implements this "water spreading" idea. By starting from all water cells simultaneously and processing cells level by level, we guarantee that:
- Each cell is reached first via its shortest path to water
- The height assigned equals this shortest distance
- No cell can have a higher height without violating the adjacency constraint
This is why BFS is perfect here - it explores the graph level by level, ensuring we find the minimum distance (and thus assign the correct height) for each cell in the optimal order.
Learn more about Breadth-First Search patterns.
Solution Approach
The implementation uses a multi-source BFS with a queue to efficiently assign heights to all cells:
1. Initialization Phase:
- Create a result matrix
ans
initialized with-1
to mark unvisited cells - Use a
deque
(double-ended queue) for BFS traversal - Scan through the entire grid to find all water cells
- For each water cell at position
(i, j)
, setans[i][j] = 0
and add(i, j)
to the queue
2. BFS Traversal:
- Process cells level by level from the queue
- For each cell
(i, j)
dequeued:- Check all 4 adjacent cells (up, down, left, right)
- The
pairwise((-1, 0, 1, 0, -1))
cleverly generates direction pairs:(-1, 0)
,(0, 1)
,(1, 0)
,(0, -1)
- For each adjacent cell
(x, y)
:- Check if it's within bounds:
0 <= x < m and 0 <= y < n
- Check if it's unvisited:
ans[x][y] == -1
- If both conditions are met:
- Assign height:
ans[x][y] = ans[i][j] + 1
- Add
(x, y)
to the queue for future processing
- Assign height:
- Check if it's within bounds:
3. Key Implementation Details:
- Using
-1
as a marker for unvisited cells serves dual purpose: tracking visited status and avoiding revisiting - The
pairwise
function with(-1, 0, 1, 0, -1)
is a compact way to generate the 4 directional offsets - BFS guarantees that when we first reach a cell, we've found its shortest distance to water
- The queue ensures we process cells in order of their distance from water (level by level)
Time Complexity: O(m Γ n)
where we visit each cell exactly once
Space Complexity: O(m Γ n)
for the result matrix and at most O(m Γ n)
for the queue in worst case
This approach elegantly solves the problem by treating it as a shortest path problem in an unweighted graph, where BFS naturally gives us the optimal solution.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the solution approach.
Consider a 3Γ4 grid where 0
represents land and 1
represents water:
isWater = [[0, 0, 1, 0], [0, 0, 0, 0], [1, 0, 0, 0]]
Step 1: Initialization
- Create result matrix
ans
filled with-1
(unvisited) - Find all water cells and set them to height
0
- Add water cells to queue
ans = [[-1, -1, 0, -1], Queue: [(0,2), (2,0)] [-1, -1, -1, -1], [ 0, -1, -1, -1]]
Step 2: Process first level (distance 1 from water)
- Dequeue
(0,2)
: Check its 4 neighbors(0,1)
: unvisited, set height = 0 + 1 = 1, add to queue(0,3)
: unvisited, set height = 0 + 1 = 1, add to queue(1,2)
: unvisited, set height = 0 + 1 = 1, add to queue
- Dequeue
(2,0)
: Check its 4 neighbors(2,1)
: unvisited, set height = 0 + 1 = 1, add to queue(1,0)
: unvisited, set height = 0 + 1 = 1, add to queue
ans = [[-1, 1, 0, 1], Queue: [(0,1), (0,3), (1,2), (2,1), (1,0)] [ 1, -1, 1, -1], [ 0, 1, -1, -1]]
Step 3: Process second level (distance 2 from water)
- Dequeue and process cells with height 1
- Each unvisited neighbor gets height = 1 + 1 = 2
After processing (0,1)
, (0,3)
, (1,2)
, (2,1)
, (1,0)
:
ans = [[2, 1, 0, 1], Queue: [(0,0), (1,1), (1,3), (2,2)] [1, 2, 1, 2], [0, 1, 2, -1]]
Step 4: Process third level (distance 3 from water)
- Process remaining cells with height 2
- Cell
(2,3)
gets height = 2 + 1 = 3
Final result:
ans = [[2, 1, 0, 1], [1, 2, 1, 2], [0, 1, 2, 3]]
The maximum height achieved is 3
at position (2,3)
, which is the land cell farthest from any water source. Notice how each cell's height equals its shortest distance to water, and adjacent cells differ by at most 1.
Solution Implementation
1from collections import deque
2from typing import List
3
4class Solution:
5 def highestPeak(self, isWater: List[List[int]]) -> List[List[int]]:
6 # Get dimensions of the grid
7 rows, cols = len(isWater), len(isWater[0])
8
9 # Initialize result matrix with -1 (unvisited cells)
10 heights = [[-1] * cols for _ in range(rows)]
11
12 # Queue for BFS traversal
13 queue = deque()
14
15 # Find all water cells and mark them as height 0
16 for row in range(rows):
17 for col in range(cols):
18 if isWater[row][col] == 1:
19 queue.append((row, col))
20 heights[row][col] = 0
21
22 # Define four directions: up, right, down, left
23 directions = [(-1, 0), (0, 1), (1, 0), (0, -1)]
24
25 # BFS to assign heights to all cells
26 while queue:
27 current_row, current_col = queue.popleft()
28
29 # Check all four adjacent cells
30 for delta_row, delta_col in directions:
31 next_row = current_row + delta_row
32 next_col = current_col + delta_col
33
34 # Check if the adjacent cell is within bounds and unvisited
35 if (0 <= next_row < rows and
36 0 <= next_col < cols and
37 heights[next_row][next_col] == -1):
38
39 # Assign height as current cell's height + 1
40 heights[next_row][next_col] = heights[current_row][current_col] + 1
41 queue.append((next_row, next_col))
42
43 return heights
44
1class Solution {
2 public int[][] highestPeak(int[][] isWater) {
3 // Get dimensions of the grid
4 int rows = isWater.length;
5 int cols = isWater[0].length;
6
7 // Initialize result matrix to store heights
8 int[][] heights = new int[rows][cols];
9
10 // Queue for BFS traversal
11 Deque<int[]> queue = new ArrayDeque<>();
12
13 // Initialize the heights matrix and add all water cells to queue
14 for (int row = 0; row < rows; row++) {
15 for (int col = 0; col < cols; col++) {
16 // Convert: water cells (1) become 0, land cells (0) become -1
17 // This marks water cells as height 0 and land cells as unvisited (-1)
18 heights[row][col] = isWater[row][col] - 1;
19
20 // Add all water cells (height 0) to the queue as starting points
21 if (heights[row][col] == 0) {
22 queue.offer(new int[] {row, col});
23 }
24 }
25 }
26
27 // Direction vectors for exploring 4 adjacent cells (up, right, down, left)
28 int[] directions = {-1, 0, 1, 0, -1};
29
30 // BFS to assign heights to all land cells
31 while (!queue.isEmpty()) {
32 // Get the next cell to process
33 int[] currentCell = queue.poll();
34 int currentRow = currentCell[0];
35 int currentCol = currentCell[1];
36
37 // Check all 4 adjacent cells
38 for (int dir = 0; dir < 4; dir++) {
39 int nextRow = currentRow + directions[dir];
40 int nextCol = currentCol + directions[dir + 1];
41
42 // Check if the adjacent cell is within bounds and unvisited (height == -1)
43 if (nextRow >= 0 && nextRow < rows &&
44 nextCol >= 0 && nextCol < cols &&
45 heights[nextRow][nextCol] == -1) {
46
47 // Assign height as current cell's height + 1
48 heights[nextRow][nextCol] = heights[currentRow][currentCol] + 1;
49
50 // Add the newly processed cell to queue for further exploration
51 queue.offer(new int[] {nextRow, nextCol});
52 }
53 }
54 }
55
56 return heights;
57 }
58}
59
1class Solution {
2public:
3 // Direction vectors for moving up, right, down, left (4-directional movement)
4 const int directions[5] = {-1, 0, 1, 0, -1};
5
6 vector<vector<int>> highestPeak(vector<vector<int>>& isWater) {
7 int rows = isWater.size();
8 int cols = isWater[0].size();
9
10 // Initialize result matrix to store height values
11 vector<vector<int>> heightMatrix(rows, vector<int>(cols));
12
13 // Queue for BFS traversal, storing cell coordinates
14 queue<pair<int, int>> bfsQueue;
15
16 // Initialize the height matrix and populate queue with water cells
17 for (int row = 0; row < rows; ++row) {
18 for (int col = 0; col < cols; ++col) {
19 // Convert: water cells (1) become 0, land cells (0) become -1
20 // -1 indicates unvisited land cells
21 heightMatrix[row][col] = isWater[row][col] - 1;
22
23 // Add all water cells to queue as starting points for BFS
24 if (heightMatrix[row][col] == 0) {
25 bfsQueue.emplace(row, col);
26 }
27 }
28 }
29
30 // BFS to assign heights to all land cells
31 while (!bfsQueue.empty()) {
32 // Get current cell coordinates
33 auto [currentRow, currentCol] = bfsQueue.front();
34 bfsQueue.pop();
35
36 // Check all 4 adjacent cells
37 for (int dir = 0; dir < 4; ++dir) {
38 int nextRow = currentRow + directions[dir];
39 int nextCol = currentCol + directions[dir + 1];
40
41 // Check if the adjacent cell is within bounds and unvisited
42 if (nextRow >= 0 && nextRow < rows &&
43 nextCol >= 0 && nextCol < cols &&
44 heightMatrix[nextRow][nextCol] == -1) {
45
46 // Assign height as current cell's height + 1
47 heightMatrix[nextRow][nextCol] = heightMatrix[currentRow][currentCol] + 1;
48
49 // Add the newly visited cell to queue for further exploration
50 bfsQueue.emplace(nextRow, nextCol);
51 }
52 }
53 }
54
55 return heightMatrix;
56 }
57};
58
1/**
2 * Calculates the height of each cell in a matrix where water cells have height 0
3 * and land cells have heights that increase by 1 for each step away from water.
4 * Uses multi-source BFS to find the minimum distance from each cell to the nearest water cell.
5 *
6 * @param isWater - 2D array where 1 represents water and 0 represents land
7 * @returns 2D array with heights assigned to each cell
8 */
9function highestPeak(isWater: number[][]): number[][] {
10 const rows: number = isWater.length;
11 const cols: number = isWater[0].length;
12
13 // Initialize result matrix with -1 (unvisited cells)
14 const heights: number[][] = [];
15
16 // Queue for BFS traversal, stores [row, col] coordinates
17 let queue: number[][] = [];
18
19 // Initialize heights matrix and add all water cells to queue
20 for (let row = 0; row < rows; row++) {
21 heights.push(new Array(cols).fill(-1));
22
23 for (let col = 0; col < cols; col++) {
24 if (isWater[row][col]) {
25 // Water cells have height 0 and are starting points for BFS
26 queue.push([row, col]);
27 heights[row][col] = 0;
28 }
29 }
30 }
31
32 // Direction vectors for exploring 4 adjacent cells (up, right, down, left)
33 const directions: number[] = [-1, 0, 1, 0, -1];
34
35 // Multi-source BFS to assign heights level by level
36 while (queue.length > 0) {
37 const nextLevelQueue: number[][] = [];
38
39 // Process all cells at current distance level
40 for (const [currentRow, currentCol] of queue) {
41 // Explore all 4 adjacent cells
42 for (let dirIndex = 0; dirIndex < 4; dirIndex++) {
43 const nextRow: number = currentRow + directions[dirIndex];
44 const nextCol: number = currentCol + directions[dirIndex + 1];
45
46 // Check if adjacent cell is valid and unvisited
47 if (nextRow >= 0 && nextRow < rows &&
48 nextCol >= 0 && nextCol < cols &&
49 heights[nextRow][nextCol] === -1) {
50
51 // Add to next level queue
52 nextLevelQueue.push([nextRow, nextCol]);
53
54 // Assign height as current cell's height + 1
55 heights[nextRow][nextCol] = heights[currentRow][currentCol] + 1;
56 }
57 }
58 }
59
60 // Move to next level
61 queue = nextLevelQueue;
62 }
63
64 return heights;
65}
66
Time and Space Complexity
Time Complexity: O(m * n)
where m
is the number of rows and n
is the number of columns in the grid.
- The initial nested loop to find all water cells and add them to the queue takes
O(m * n)
time as it visits every cell once. - The BFS traversal processes each cell exactly once. When a cell is processed, it's marked with a value (no longer -1), and only cells with value -1 are added to the queue. Therefore, each of the
m * n
cells is added to and removed from the queue at most once. - For each cell processed, we check at most 4 neighbors using the
pairwise
function, which isO(1)
operation per cell. - Total time:
O(m * n) + O(m * n) * O(1) = O(m * n)
Space Complexity: O(m * n)
- The answer matrix
ans
requiresO(m * n)
space to store the height values for all cells. - The queue
q
can contain at mostO(m * n)
elements in the worst case (though typically much less - at most all water cells initially, then frontier cells during BFS). - The
pairwise
operation creates temporary tuples but usesO(1)
additional space. - Total space:
O(m * n) + O(m * n) = O(m * n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Using DFS Instead of BFS
A common mistake is attempting to solve this problem with DFS, which doesn't guarantee the shortest path to water cells. DFS might assign suboptimal heights because it explores one path deeply before backtracking.
Why it fails: DFS could reach a land cell through a longer path first, assigning it a higher value than necessary. Later, when a shorter path is found, the cell is already visited and won't be updated.
Solution: Always use BFS for shortest path problems in unweighted graphs. BFS explores level by level, ensuring the first time we reach a cell is via the shortest path.
2. Not Using Multi-Source BFS
Some might try to run BFS from each water cell individually, which leads to incorrect results or inefficiency.
Why it fails: Running separate BFS from each water source would require complex logic to determine which water source should "win" for each land cell, and would have O(m Γ n Γ k) time complexity where k is the number of water cells.
Solution: Initialize all water cells in the queue simultaneously. This treats all water cells as starting points at distance 0, naturally computing the minimum distance to any water cell.
3. Incorrect Visited Cell Tracking
Using a separate boolean matrix for tracking visited cells or checking isWater
array instead of the result matrix.
Why it fails: This adds unnecessary space complexity and can lead to synchronization issues between the visited status and height assignments.
Solution: Use the result matrix itself with -1
as the unvisited marker. This serves dual purpose: tracking visited status and storing the final heights.
4. Processing the Same Cell Multiple Times
Forgetting to mark cells as visited immediately when adding them to the queue.
# WRONG: Mark as visited when dequeuing while queue: row, col = queue.popleft() if heights[row][col] != -1: # Already visited continue heights[row][col] = some_value # Too late! # CORRECT: Mark as visited when enqueuing if heights[next_row][next_col] == -1: heights[next_row][next_col] = heights[current_row][current_col] + 1 queue.append((next_row, next_col))
Why it fails: The same cell could be added to the queue multiple times from different neighbors, leading to redundant processing and potentially incorrect results.
Solution: Always mark the cell as visited (assign its height) immediately when adding it to the queue, not when dequeuing it.
Which of the following is a min heap?
Recommended Readings
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Donβt Miss This!