trapping-rain-water

::: info Prerequisite

Before reading this article, you need to learn:

:::

LeetCode Problem 42 "Trapping Rain Waterarrow-up-right" is interesting and often appears in interviews. This article will explain how to optimize the solution step by step.

Here is the problem:

LeetCode 42. Trapping Rain Water

Given n non-negative integers representing an elevation map where the width of each bar is 1, compute how much water it can trap after raining.

Example 1:**

Example 2:**

Constraints:

  • n == height.length

  • 1 <= n <= 2 * 10^(4)

  • 0 <= height[i] <= 10^(5)

You are given an array representing a bar chart. The question asks how much water can be trapped by the bars after raining.

Next, I will introduce three methods from simple to advanced: brute-force solution -> memoization solution -> two pointers solution. We will solve this problem in O(N) time and O(1) space.

1. Core Idea

::: tip Quick Tip

When solving algorithm problems, if you have no idea how to start, try to simplify the problem. Think about a small part first and write the simplest brute-force solution. You might find a key point and then optimize step by step to reach the best solution.

:::

For this problem, let's not think about the whole bar chart first. Just think about how much water can be trapped at position i?

At position i, we can trap 2 units of water. This is because height[i] is 0, and the highest possible water level here is 2. So, 2 - 0 = 2.

Why can position i hold up to 2 units of water? It depends on the tallest bar to the left and the tallest bar to the right of position i. Let's call these heights l_max and r_max. The highest water level at position i is min(l_max, r_max).

So, for position i, the amount of water trapped is:

This is the main idea of this problem. We can write a simple brute-force algorithm:

This solution is very straightforward. The time complexity is O(N^2), and space complexity is O(1). But calculating r_max and l_max this way is not smart, because we have to loop every time. Can we make this better?

2. Memoization Optimization

In the brute-force solution, at each position i, we need to calculate r_max and l_max. Instead of calculating them every time, we can calculate the results in advance and store them. This will reduce the time complexity.

We can use two arrays, r_max and l_max, as memoization. l_max[i] means the highest bar on the left of position i, and r_max[i] means the highest bar on the right of position i. We calculate these two arrays first, so we don't repeat work:

This optimization is similar to the brute-force method, but it avoids repeated calculation. The time complexity becomes O(N), which is the best possible. But the space complexity is O(N). Next, let's look at a clever solution that reduces space complexity to O(1).

3. Two Pointer Solution

::: note My Advice

This solution is for expanding your thinking. You don't have to focus too much on finding the best solution. For most people, in real interviews or tests, it is enough to use simple and clear methods like the one above. Although it uses a bit more space, most online judges will still accept it.

Unless you cannot pass all test cases, and you have finished other questions with time left, you can then spend time improving the above solution.

:::

The idea of this solution is the same, but the implementation is clever. This time, we do not use extra arrays to pre-calculate the values. Instead, we use two pointers to calculate while moving, which saves space.

First, take a look at this piece of code:

For this code, what do l_max and r_max mean?

It is easy to understand. l_max means the highest bar from height[0..left], and r_max means the highest bar from height[right..end].

With this in mind, let's look at the solution:

You see, the core idea is exactly the same as before. But careful readers might notice a small difference:

In the previous memoization solution, l_max[i] and r_max[i] mean the highest bar from height[0..i] and height[i..end].

But in the two pointer solution, l_max and r_max mean the highest bars from height[0..left] and height[right..end]. For example, look at this code:

Here, l_max is the highest bar to the left of the left pointer. But r_max is not always the highest bar to the right of left. Can this still give the right answer?

The key is, we only care about min(l_max, r_max). In the picture above, we already know l_max < r_max. It does not matter if r_max is the largest on the right. What matters is the water at height[i] only depends on the lower value, which is l_max.

In this way, the trapping rain water problem is solved.

Extension: Container With Most Water

Now let's look at a problem very similar to the "Trapping Rain Water" problem. It's LeetCode problem 11: "Container With Most Water"arrow-up-right:

LeetCode 11. Container With Most Water

You are given an integer array height of length n. There are n vertical lines drawn such that the two endpoints of the i^(th) line are (i, 0) and (i, height[i]).

Find two lines that together with the x-axis form a container, such that the container contains the most water.

Return the maximum amount of water a container can store.

Notice that you may not slant the container.

Example 1:**

Example 2:**

Constraints:

  • n == height.length

  • 2 <= n <= 10^(5)

  • 0 <= height[i] <= 10^(4)

This problem is similar to the rain water problem, and you can use the same method. In fact, it is even more simple. The difference is:

In the rain water problem, each x-coordinate has a bar with width. In this problem, each x-coordinate has only a vertical line, no width.

Earlier, we talked a lot about l_max and r_max. They are used to figure out how much water height[i] can hold. But in this problem, since height[i] has no width, it becomes much easier.

For example, in the rain water problem, if you know the heights of height[left] and height[right], can you calculate how much water is between left and right?

You can't, because you don't know how much water each bar in between can hold. You need the l_max and r_max for each bar to get the answer.

But in this problem, if you know the heights of height[left] and height[right], can you work out the water held between left and right?

Yes, because in this problem the lines have no width. The water between left and right is:

Just like in the rain water problem, the height is decided by the smaller of height[left] and height[right].

The way to solve this problem is still the two-pointer technique:

Use two pointers, left and right, starting from both ends and moving towards the center. For each pair [left, right], calculate the area, and keep the biggest area as the answer.

Let's look at the code:

The code is almost the same as the rain water problem. But you may wonder, why do we move the lower side in this if statement:

It is easy to understand, because the height of the rectangle is decided by the smaller of height[left] and height[right].

If you move the lower side, that side may get higher, and the rectangle height may get bigger, so the area might also get bigger. If you move the higher side, the height will never get bigger, so the area cannot get bigger.

That's it. This problem is solved.

Last updated