Trapping Rain: A LeetCode Problem Explained
Trapping Rain: A LeetCode Problem Explained
Hey everyone, and welcome back to the blog! Today, we’re diving deep into a classic problem that pops up all the time in coding interviews and on platforms like LeetCode: the Trapping Rain Water problem . If you’ve ever stared at a list of non-negative integers representing an elevation map and wondered how much water could possibly get trapped between those bars, you’re in the right place. This isn’t just about solving a puzzle; it’s about understanding the core logic that makes efficient solutions possible. We’ll break it down, explore different approaches, and hopefully, you’ll walk away feeling super confident about tackling this one. So, grab your favorite beverage, settle in, and let’s get our hands dirty with some algorithmic thinking!
Table of Contents
Understanding the Core Concept: What is Trapping Rain Water?
Alright guys, let’s get down to business. The
Trapping Rain Water problem
, at its heart, asks us to calculate the total amount of water that can be held by a series of vertical bars of varying heights. Imagine these bars form a container, and rain starts pouring down. The water gets trapped in the depressions formed by taller bars on either side. The key insight here is that the amount of water trapped above any given bar depends on the
height of the tallest bar to its left
and the
height of the tallest bar to its right
. The water level above a specific bar will be limited by the
shorter
of these two surrounding maximums. Specifically, for any bar at index
i
, the water trapped above it is
min(max_left, max_right) - height[i]
, but
only if
min(max_left, max_right)
is greater than
height[i]
. If
height[i]
is taller than or equal to the surrounding maximums, no water can be trapped above it. We need to sum this trapped water amount for
all
the bars in the elevation map to get our final answer. It sounds straightforward, but implementing it efficiently is where the real challenge lies. We need to consider how to find these
max_left
and
max_right
values for every single bar in the array. This is the fundamental principle we’ll be building upon as we explore different solution strategies. Think of it like building a dam; the water level is dictated by the lowest point of the surrounding walls, not the highest. This analogy is crucial for visualizing how water gets held back.
Approach 1: Brute Force - The Naive (but understandable) Way
So, let’s start with the most intuitive approach, the
brute force method
. This is often the first thing that comes to mind when you’re trying to solve the Trapping Rain Water problem, and it’s a great starting point for understanding the logic. For
every single bar
in our elevation map (represented by an array of heights), we need to figure out how much water can be trapped
above that specific bar
. To do this, we need two pieces of information for each bar at index
i
: the
maximum height of a bar to its left
(including itself, though it doesn’t matter for water trapping) and the
maximum height of a bar to its right
(again, including itself). Let’s call these
max_left
and
max_right
. We can find
max_left
by iterating from the beginning of the array up to index
i
and finding the tallest bar. Similarly, we find
max_right
by iterating from index
i
all the way to the end of the array and finding the tallest bar. Once we have
max_left
and
max_right
for bar
i
, the amount of water that can be trapped above it is determined by the
minimum
of these two maximums, minus the height of the bar itself:
water_at_i = min(max_left, max_right) - height[i]
. Now, here’s a crucial point: if
min(max_left, max_right)
is
less than or equal to
height[i]
, then no water can be trapped above this bar, so
water_at_i
would be 0. We only add positive amounts of water. We then repeat this process for
every single bar
in the array, from the first to the last. Finally, we sum up all the
water_at_i
values calculated for each bar to get the total amount of trapped rainwater. While this brute force approach is easy to understand conceptually, let’s talk about its performance. For each bar, we’re doing two scans (one to the left, one to the right) to find the maximums. If our array has
n
bars, finding
max_left
takes roughly
O(i)
time, and finding
max_right
takes roughly
O(n-i)
time. Doing this for all
n
bars results in a total time complexity of approximately
O(n^2)
. This is because for each of the
n
bars, we’re iterating through potentially
n
other bars. For large input arrays, this can become quite slow. The space complexity, however, is excellent – we’re just using a few variables, so it’s
O(1)
extra space. It’s a solid starting point, but we can definitely do better, right?
Approach 2: Dynamic Programming - Precomputing Maximums
Okay, so the brute force method works, but
O(n^2)
isn’t ideal for a problem that might involve a large number of bars. The bottleneck in the brute force approach is repeatedly calculating the
max_left
and
max_right
for each bar. We’re doing a lot of redundant work! This is where
dynamic programming
comes to the rescue. The core idea is to precompute the
max_left
and
max_right
values for
all
bars in separate arrays. This way, when we iterate through the bars to calculate the trapped water, we can simply look up these precomputed values in
O(1)
time instead of recalculating them. Let’s break this down. First, we’ll create an array, say
left_max
, of the same size as our input
height
array. We’ll iterate through the
height
array from left to right.
left_max[i]
will store the maximum height encountered from index 0 up to index
i
. So,
left_max[0]
will just be
height[0]
. For any subsequent index
i
,
left_max[i]
will be
max(left_max[i-1], height[i])
. This single pass fills our
left_max
array. Now, we do a similar thing for the right side. We create another array,
right_max
, also of the same size. We iterate through the
height
array from right to left.
right_max[i]
will store the maximum height encountered from index
n-1
(the end) down to index
i
. So,
right_max[n-1]
will be
height[n-1]
. For any preceding index
i
,
right_max[i]
will be
max(right_max[i+1], height[i])
. Once we have both
left_max
and
right_max
arrays populated, we can calculate the trapped water. We iterate through the
height
array one more time, from index 1 to
n-2
(the first and last bars can’t trap water). For each bar at index
i
, the amount of water trapped is
min(left_max[i], right_max[i]) - height[i]
. Again, if this value is negative or zero, we take it as 0. We sum up these amounts for all
i
. This dynamic programming approach significantly improves our time complexity. We have three passes through the array: one for
left_max
, one for
right_max
, and one for calculating the total water. Each pass takes
O(n)
time. Therefore, the overall time complexity is
O(n) + O(n) + O(n)
, which simplifies to
O(n)
. This is a huge improvement over
O(n^2)
! However, there’s a trade-off. We are now using two extra arrays,
left_max
and
right_max
, each of size
n
. So, the space complexity becomes
O(n)
. It’s a classic space-time trade-off: we use more memory to gain speed. Pretty neat, huh? This DP approach is a very common and efficient way to solve the Trapping Rain Water problem.
Approach 3: Two Pointers - The Space-Optimized Solution
Alright folks, we’ve seen the brute force method and the dynamic programming approach. The DP method got us down to
O(n)
time complexity, which is fantastic, but it used
O(n)
extra space for those
left_max
and
right_max
arrays. What if we could achieve
O(n)
time complexity
without
using that extra
O(n)
space? That’s exactly what the
two-pointers approach
does, and it’s often considered the most elegant and efficient solution for the Trapping Rain Water problem. The intuition behind this method is quite clever. Instead of precomputing all
max_left
and
max_right
values, we use two pointers, one starting at the beginning of the array (let’s call it
left = 0
) and the other at the end of the array (let’s call it
right = n-1
). We also maintain two variables,
max_left
and
max_right
, initialized to 0. These variables will keep track of the maximum height encountered so far from the left and right sides, respectively, as our pointers move inwards. The core logic is this: at each step, we compare
height[left]
and
height[right]
. If
height[left]
is less than
height[right]
, it means that the water level at the
left
pointer’s position is potentially limited by
height[left]
(or the
max_left
we’ve seen so far). Why? Because we
know
there’s a bar to the right (
height[right]
and potentially even taller ones beyond it) that is at least as tall as
height[left]
. So, the
max_right
for
height[left]
is guaranteed to be at least
height[right]
, which is taller than
height[left]
. Therefore, the water trapped at
left
depends only on
max_left
. If
height[left]
is greater than or equal to
max_left
, we update
max_left = height[left]
because this bar can potentially form a new boundary for trapping water. If
height[left]
is less than
max_left
, then we can trap
max_left - height[left]
amount of water at this position, and we add this to our total trapped water. After processing the
left
pointer, we increment
left
. Conversely, if
height[right]
is less than or equal to
height[left]
, we apply the same logic but from the right side. The water level at
right
is potentially limited by
height[right]
(or
max_right
). We know
max_left
is at least
height[left]
, which is taller than
height[right]
. So, the water trapped at
right
depends only on
max_right
. If
height[right]
is greater than or equal to
max_right
, we update
max_right = height[right]
. If
height[right]
is less than
max_right
, we trap
max_right - height[right]
water and add it to our total. Then, we decrement
right
. We continue this process, moving the
left
pointer inwards from the left and the
right
pointer inwards from the right, until
left
crosses
right
(
left >= right
). This entire process involves a single pass through the array, with each pointer moving towards the center. Thus, the time complexity is
O(n)
. And the best part? We are only using a few extra variables (
left
,
right
,
max_left
,
max_right
,
total_water
), so the space complexity is
O(1)
. This is the gold standard for solving the Trapping Rain Water problem – optimal time and optimal space. Pretty awesome, right?
Conclusion: Mastering the Trapping Rain Water Problem
So there you have it, guys! We’ve journeyed through the
Trapping Rain Water problem
, starting from the straightforward yet inefficient brute force method, moving on to the more optimized dynamic programming approach, and finally arriving at the elegant and highly efficient two-pointers solution. Each method builds upon the understanding of the core principle: water trapped at any point is limited by the minimum of the maximum heights to its left and right. The brute force method helps solidify this concept but struggles with performance at
O(n^2)
time complexity. The dynamic programming approach smartly precomputes these maximums, bringing the time complexity down to
O(n)
at the cost of
O(n)
extra space. Finally, the two-pointers technique is the champion, achieving
O(n)
time complexity with just
O(1)
space by cleverly processing from both ends inwards. Understanding these different solutions not only helps you crack this specific problem but also sharpens your algorithmic thinking. You learn to identify repeated computations, leverage precomputation, and optimize space usage. When you encounter problems like this in interviews or coding challenges, think about: 1. What’s the most intuitive way to solve it? 2. Can I optimize the time complexity? 3. Can I optimize the space complexity? Often, there’s a trade-off, and knowing when to use which approach is key. The Trapping Rain Water problem is a fantastic example of how a bit of cleverness can transform a slow solution into a lightning-fast one. Keep practicing, keep thinking critically, and you’ll be a pro at solving these kinds of problems in no time! Happy coding!