755. Pour Water 🔒
Problem Description
You have a terrain represented as an array heights
where heights[i]
is the height of the terrain at position i
. Each position has a width of 1 unit. You need to pour volume
units of water, one unit at a time, starting at position k
.
When each unit of water is poured at position k
, it follows these movement rules:
- Try to move left: If the water can eventually fall to a lower level by moving left, it will flow left
- Otherwise, try to move right: If the water can't fall left but can eventually fall to a lower level by moving right, it will flow right
- Otherwise, stay: If the water can't fall in either direction, it accumulates at position
k
The term "eventually fall" means the water will reach a position with a lower total level (terrain height + accumulated water) if it keeps moving in that direction. When water moves in a direction, it flows to the lowest reachable position in that direction.
Key constraints:
- The terrain extends infinitely high on both sides outside the array bounds (water cannot overflow the edges)
- Each unit of water must occupy exactly one position (no splitting across multiple positions)
- Water accumulates on top of terrain and previously poured water
The goal is to return the final state of the array after pouring all volume
units of water, where each element represents the total height (original terrain + accumulated water) at that position.
For example, if heights = [2,1,1,2,1,2,2]
, volume = 4
, and k = 3
, the water poured at index 3 will flow and accumulate at the lower positions, resulting in the final configuration.
Intuition
The key insight is that we need to simulate each water drop individually because the placement of previous drops affects where the next drop will land. Each drop follows a predictable pattern: it tries to flow to the lowest reachable point.
When a water drop lands at position k
, we need to explore both directions to find where it should settle. The critical observation is that water follows a greedy approach - it always prefers to move left over right (according to the problem rules), and it will flow to the lowest point it can reach in that direction.
For each direction, we can scan outward from position k
and track two things:
- Whether we can continue moving (the next position is not higher than our current position)
- The lowest position we've encountered so far
The scanning process continues as long as heights[i + d] <= heights[i]
, meaning we can either move to the same level or drop down. During this scan, whenever we find a strictly lower position (heights[i + d] < heights[i]
), we update our target position.
The elegant part of this solution is using a loop over directions d in (-1, 1)
where -1 represents left and 1 represents right. By checking left first, we naturally implement the rule "if water can fall left, go left; otherwise try right." The for-else
construct in Python perfectly handles the case where water can't fall in either direction - if we break out of the direction loop (water found a place to fall), the else
block doesn't execute; if we complete both directions without breaking (water can't fall anywhere), the else
block executes and places water at position k
.
This simulation approach works because water behaves deterministically - given the same terrain configuration, a drop at position k
will always end up in the same place. By processing drops one at a time and updating the heights array after each drop, we accurately model how the water accumulates and affects subsequent drops.
Solution Approach
The solution uses a simulation approach to process each unit of water individually. Here's how the implementation works:
Main Loop Structure:
for _ in range(volume):
We iterate volume
times, processing one unit of water in each iteration.
Direction Exploration:
for d in (-1, 1):
For each water unit, we explore two directions: left (d = -1
) and right (d = 1
). By checking left first, we implement the rule that water prefers to flow left.
Scanning for the Lowest Point:
i = j = k
while 0 <= i + d < len(heights) and heights[i + d] <= heights[i]:
if heights[i + d] < heights[i]:
j = i + d
i += d
- Initialize both
i
(current position) andj
(target position) tok
- Continue scanning in direction
d
as long as:- We're within array bounds:
0 <= i + d < len(heights)
- The next position is not higher:
heights[i + d] <= heights[i]
- We're within array bounds:
- During the scan, if we find a strictly lower position (
heights[i + d] < heights[i]
), we updatej
to remember this as our target position - Move to the next position:
i += d
Water Placement Logic:
if j != k: heights[j] += 1 break else: heights[k] += 1
- If
j != k
, it means we found a lower position in the current direction, so we place water there andbreak
out of the direction loop - If we complete both directions without finding a place to drop (the
for-else
construct), water stays at positionk
Time and Space Complexity:
- Time Complexity:
O(volume × n)
wheren
is the length of the heights array. For each unit of water, we potentially scan the entire array in the worst case. - Space Complexity:
O(1)
as we only use a constant amount of extra space (variablesi
,j
,d
).
The beauty of this implementation is its simplicity - by processing water drops one at a time and always choosing the leftmost lowest point, we naturally implement all the problem's rules without complex logic or data structures.
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 with heights = [3, 1, 3]
, volume = 2
, and k = 1
(pouring at the middle position).
Initial State:
Position: 0 1 2 Heights: 3 1 3
Pouring Unit 1:
- Start at position k=1 (height=1)
- Check left direction (d=-1):
- Position 0 has height 3, which is higher than 1
- Cannot move left (3 > 1), so j remains 1
- Check right direction (d=1):
- Position 2 has height 3, which is higher than 1
- Cannot move right (3 > 1), so j remains 1
- Since j=k=1 after checking both directions, water stays at position 1
- Update:
heights[1] = 1 + 1 = 2
After Unit 1:
Position: 0 1 2 Heights: 3 2 3
Pouring Unit 2:
- Start at position k=1 (height=2 now)
- Check left direction (d=-1):
- Position 0 has height 3, which is higher than 2
- Cannot move left (3 > 2), so j remains 1
- Check right direction (d=1):
- Position 2 has height 3, which is higher than 2
- Cannot move right (3 > 2), so j remains 1
- Since j=k=1 after checking both directions, water stays at position 1
- Update:
heights[1] = 2 + 1 = 3
Final State:
Position: 0 1 2 Heights: 3 3 3
The water filled up the valley at position 1, bringing it to the same level as its neighbors. This demonstrates how water accumulates when it cannot flow to a lower position in either direction.
Solution Implementation
1class Solution:
2 def pourWater(self, heights: List[int], volume: int, k: int) -> List[int]:
3 # Process each water drop one by one
4 for _ in range(volume):
5 # Try to pour water to the left (-1) first, then right (1)
6 for direction in (-1, 1):
7 current_pos = k # Current position being examined
8 lowest_pos = k # Position of the lowest point found
9
10 # Explore in the current direction while valid and not going uphill
11 while 0 <= current_pos + direction < len(heights) and \
12 heights[current_pos + direction] <= heights[current_pos]:
13 # If we find a strictly lower position, update the lowest position
14 if heights[current_pos + direction] < heights[current_pos]:
15 lowest_pos = current_pos + direction
16 # Move to the next position in the current direction
17 current_pos += direction
18
19 # If we found a lower position than starting point, pour water there
20 if lowest_pos != k:
21 heights[lowest_pos] += 1
22 break # Water has been placed, move to next drop
23 else:
24 # If water couldn't flow left or right, it stays at position k
25 heights[k] += 1
26
27 return heights
28
1class Solution {
2 public int[] pourWater(int[] heights, int volume, int k) {
3 // Process each unit of water one by one
4 while (volume-- > 0) {
5 boolean waterPlaced = false;
6
7 // Try both directions: left (-1) first, then right (+1)
8 for (int direction = -1; direction < 2 && !waterPlaced; direction += 2) {
9 int currentIndex = k;
10 int lowestIndex = k;
11
12 // Explore in the current direction while within bounds and not going uphill
13 while (currentIndex + direction >= 0 &&
14 currentIndex + direction < heights.length &&
15 heights[currentIndex + direction] <= heights[currentIndex]) {
16
17 // Update lowest point if we find a strictly lower position
18 if (heights[currentIndex + direction] < heights[currentIndex]) {
19 lowestIndex = currentIndex + direction;
20 }
21
22 // Move to the next position in current direction
23 currentIndex += direction;
24 }
25
26 // If we found a lower position, place water there
27 if (lowestIndex != k) {
28 waterPlaced = true;
29 heights[lowestIndex]++;
30 }
31 }
32
33 // If water couldn't flow left or right, place it at position k
34 if (!waterPlaced) {
35 heights[k]++;
36 }
37 }
38
39 return heights;
40 }
41}
42
1class Solution {
2public:
3 vector<int> pourWater(vector<int>& heights, int volume, int k) {
4 // Pour water droplets one by one
5 while (volume--) {
6 bool dropletPlaced = false;
7
8 // Try both directions: left (-1) first, then right (+1)
9 for (int direction = -1; direction <= 1 && !dropletPlaced; direction += 2) {
10 int currentPos = k; // Current position being checked
11 int lowestPos = k; // Lowest position found so far
12
13 // Explore in the current direction while within bounds and not going uphill
14 while (currentPos + direction >= 0 &&
15 currentPos + direction < heights.size() &&
16 heights[currentPos + direction] <= heights[currentPos]) {
17
18 // Update lowest position if we found a strictly lower point
19 if (heights[currentPos + direction] < heights[currentPos]) {
20 lowestPos = currentPos + direction;
21 }
22
23 // Move to next position in current direction
24 currentPos += direction;
25 }
26
27 // If we found a lower position than starting point, place water there
28 if (lowestPos != k) {
29 dropletPlaced = true;
30 ++heights[lowestPos];
31 }
32 }
33
34 // If water couldn't flow left or right, it stays at position k
35 if (!dropletPlaced) {
36 ++heights[k];
37 }
38 }
39
40 return heights;
41 }
42};
43
1function pourWater(heights: number[], volume: number, k: number): number[] {
2 // Pour water droplets one by one
3 while (volume--) {
4 let dropletPlaced = false;
5
6 // Try both directions: left (-1) first, then right (+1)
7 for (let direction = -1; direction <= 1 && !dropletPlaced; direction += 2) {
8 let currentPos = k; // Current position being checked
9 let lowestPos = k; // Lowest position found so far
10
11 // Explore in the current direction while within bounds and not going uphill
12 while (currentPos + direction >= 0 &&
13 currentPos + direction < heights.length &&
14 heights[currentPos + direction] <= heights[currentPos]) {
15
16 // Update lowest position if we found a strictly lower point
17 if (heights[currentPos + direction] < heights[currentPos]) {
18 lowestPos = currentPos + direction;
19 }
20
21 // Move to next position in current direction
22 currentPos += direction;
23 }
24
25 // If we found a lower position than starting point, place water there
26 if (lowestPos !== k) {
27 dropletPlaced = true;
28 heights[lowestPos]++;
29 }
30 }
31
32 // If water couldn't flow left or right, it stays at position k
33 if (!dropletPlaced) {
34 heights[k]++;
35 }
36 }
37
38 return heights;
39}
40
Time and Space Complexity
Time Complexity: O(v × n)
For each water droplet (total v
droplets), the algorithm searches for the lowest position where the water can settle. In the worst case, this search examines the entire array in both directions from position k
. The inner while loop can iterate up to n
positions (where n
is the length of the heights array) for each direction. Since we process v
water droplets and each droplet can potentially scan O(n)
positions, the overall time complexity is O(v × n)
.
Space Complexity: O(1)
The algorithm only uses a constant amount of extra space for variables i
, j
, and d
, regardless of the input size. The modifications are made in-place to the input heights
array, and no additional data structures that scale with input size are created. Therefore, the space complexity is O(1)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Incorrect Handling of Equal Heights
The Problem:
A common mistake is to stop scanning when encountering an equal height position, rather than continuing to look for a lower position beyond it. For example, with heights = [3, 1, 3, 1, 3]
and water poured at index 2, some might incorrectly stop at index 1 (height 1) without checking index 0 (also height 1), missing that water should flow to the leftmost position of equal height.
Incorrect Implementation:
# WRONG: Stops at first non-increasing position
while 0 <= i + d < len(heights):
if heights[i + d] < heights[i]:
j = i + d
break # Wrong! Stops too early
elif heights[i + d] == heights[i]:
i += d
else:
break
Correct Solution: Continue scanning through equal heights to find the lowest reachable position:
while 0 <= i + d < len(heights) and heights[i + d] <= heights[i]:
if heights[i + d] < heights[i]:
j = i + d # Update lowest, but keep scanning
i += d
Pitfall 2: Not Understanding "Eventually Fall" Concept
The Problem:
Some implementations only check the immediate neighbor instead of scanning until hitting a wall (higher position). For instance, with heights = [2, 2, 1, 2]
and water at index 1, the water should "eventually fall" to index 2 by flowing over the equal height at index 1.
Incorrect Implementation:
# WRONG: Only checks immediate neighbor
if k > 0 and heights[k-1] < heights[k]:
heights[k-1] += 1
elif k < len(heights)-1 and heights[k+1] < heights[k]:
heights[k+1] += 1
else:
heights[k] += 1
Correct Solution: Scan continuously in each direction until finding a higher wall or array boundary:
while 0 <= current_pos + direction < len(heights) and \
heights[current_pos + direction] <= heights[current_pos]:
# Keep scanning to find eventual fall position
if heights[current_pos + direction] < heights[current_pos]:
lowest_pos = current_pos + direction
current_pos += direction
Pitfall 3: Modifying Heights During Scanning
The Problem: Updating the heights array while still determining where the current water unit should go can lead to incorrect decisions for subsequent scans within the same water unit placement.
Incorrect Implementation:
# WRONG: Modifying during scan affects decision making
for direction in (-1, 1):
i = k
while 0 <= i + direction < len(heights):
if heights[i + direction] < heights[i]:
heights[i + direction] += 1 # Wrong! Modified too early
break
i += direction
Correct Solution: Complete all scanning for the current water unit first, then modify the array only once after determining the final position:
for direction in (-1, 1):
# First, find where water should go
current_pos = k
lowest_pos = k
while 0 <= current_pos + direction < len(heights) and \
heights[current_pos + direction] <= heights[current_pos]:
if heights[current_pos + direction] < heights[current_pos]:
lowest_pos = current_pos + direction
current_pos += direction
# Then, place the water
if lowest_pos != k:
heights[lowest_pos] += 1
break
else:
heights[k] += 1
What's the output of running the following function using input [30, 20, 10, 100, 33, 12]
?
1def fun(arr: List[int]) -> List[int]:
2 import heapq
3 heapq.heapify(arr)
4 res = []
5 for i in range(3):
6 res.append(heapq.heappop(arr))
7 return res
8
1public static int[] fun(int[] arr) {
2 int[] res = new int[3];
3 PriorityQueue<Integer> heap = new PriorityQueue<>();
4 for (int i = 0; i < arr.length; i++) {
5 heap.add(arr[i]);
6 }
7 for (int i = 0; i < 3; i++) {
8 res[i] = heap.poll();
9 }
10 return res;
11}
12
1class HeapItem {
2 constructor(item, priority = item) {
3 this.item = item;
4 this.priority = priority;
5 }
6}
7
8class MinHeap {
9 constructor() {
10 this.heap = [];
11 }
12
13 push(node) {
14 // insert the new node at the end of the heap array
15 this.heap.push(node);
16 // find the correct position for the new node
17 this.bubble_up();
18 }
19
20 bubble_up() {
21 let index = this.heap.length - 1;
22
23 while (index > 0) {
24 const element = this.heap[index];
25 const parentIndex = Math.floor((index - 1) / 2);
26 const parent = this.heap[parentIndex];
27
28 if (parent.priority <= element.priority) break;
29 // if the parent is bigger than the child then swap the parent and child
30 this.heap[index] = parent;
31 this.heap[parentIndex] = element;
32 index = parentIndex;
33 }
34 }
35
36 pop() {
37 const min = this.heap[0];
38 this.heap[0] = this.heap[this.size() - 1];
39 this.heap.pop();
40 this.bubble_down();
41 return min;
42 }
43
44 bubble_down() {
45 let index = 0;
46 let min = index;
47 const n = this.heap.length;
48
49 while (index < n) {
50 const left = 2 * index + 1;
51 const right = left + 1;
52
53 if (left < n && this.heap[left].priority < this.heap[min].priority) {
54 min = left;
55 }
56 if (right < n && this.heap[right].priority < this.heap[min].priority) {
57 min = right;
58 }
59 if (min === index) break;
60 [this.heap[min], this.heap[index]] = [this.heap[index], this.heap[min]];
61 index = min;
62 }
63 }
64
65 peek() {
66 return this.heap[0];
67 }
68
69 size() {
70 return this.heap.length;
71 }
72}
73
74function fun(arr) {
75 const heap = new MinHeap();
76 for (const x of arr) {
77 heap.push(new HeapItem(x));
78 }
79 const res = [];
80 for (let i = 0; i < 3; i++) {
81 res.push(heap.pop().item);
82 }
83 return res;
84}
85
Recommended Readings
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
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!