接雨水.md 11.2 KB
Newer Older
L
labuladong 已提交
1 2
# 接雨水问题详解

3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22

<p align='center'>
<a href="https://github.com/labuladong/fucking-algorithm" target="view_window"><img alt="GitHub" src="https://img.shields.io/github/stars/labuladong/fucking-algorithm?label=Stars&style=flat-square&logo=GitHub"></a>
<a href="https://www.zhihu.com/people/labuladong"><img src="https://img.shields.io/badge/%E7%9F%A5%E4%B9%8E-@labuladong-000000.svg?style=flat-square&logo=Zhihu"></a>
<a href="https://i.loli.net/2020/10/10/MhRTyUKfXZOlQYN.jpg"><img src="https://img.shields.io/badge/公众号-@labuladong-000000.svg?style=flat-square&logo=WeChat"></a>
<a href="https://space.bilibili.com/14089380"><img src="https://img.shields.io/badge/B站-@labuladong-000000.svg?style=flat-square&logo=Bilibili"></a>
</p>

![](../pictures/souyisou.png)

相关推荐:
  * [手把手带你刷二叉树(第三期)](https://labuladong.gitbook.io/algo)
  * [45张图解:IP基础知识全家桶](https://labuladong.gitbook.io/algo)

读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目:

[42.接雨水](https://leetcode-cn.com/problems/trapping-rain-water)

**-----------**

L
labuladong 已提交
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
接雨水这道题目挺有意思,在面试题中出现频率还挺高的,本文就来步步优化,讲解一下这道题。

先看一下题目:

![](../pictures/接雨水/title.png)

就是用一个数组表示一个条形图,问你这个条形图最多能接多少水。

```java
int trap(int[] height);
```

下面就来由浅入深介绍暴力解法 -> 备忘录解法 -> 双指针解法,在 O(N) 时间 O(1) 空间内解决这个问题。

### 一、核心思路

39
所以对于这种问题,我们不要想整体,而应该去想局部;就像之前的文章写的动态规划问题处理字符串问题,不要考虑如何处理整个字符串,而是去思考应该如何处理每一个字符。
L
labuladong 已提交
40

41
这么一想,可以发现这道题的思路其实很简单。具体来说,仅仅对于位置 `i`,能装下多少水呢?
L
labuladong 已提交
42 43 44

![](../pictures/接雨水/0.jpg)

45
能装 2 格水,因为 `height[i]` 的高度为 0,而这里最多能盛 2 格水,2-0=2。
L
labuladong 已提交
46

47
为什么位置 `i` 最多能盛 2 格水呢?因为,位置 `i` 能达到的水柱高度和其左边的最高柱子、右边的最高柱子有关,我们分别称这两个柱子高度为 `l_max``r_max`**位置 i 最大的水柱高度就是 `min(l_max, r_max)`。**
L
labuladong 已提交
48

49
更进一步,对于位置 `i`,能够装的水为:
L
labuladong 已提交
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69

```python
water[i] = min(
               # 左边最高的柱子
               max(height[0..i]),  
               # 右边最高的柱子
               max(height[i..end]) 
            ) - height[i]
    
```

![](../pictures/%E6%8E%A5%E9%9B%A8%E6%B0%B4/1.jpg)

![](../pictures/%E6%8E%A5%E9%9B%A8%E6%B0%B4/2.jpg)

这就是本问题的核心思路,我们可以简单写一个暴力算法:

```cpp
int trap(vector<int>& height) {
    int n = height.size();
70
    int res = 0;
L
labuladong 已提交
71 72 73 74 75 76 77 78 79 80
    for (int i = 1; i < n - 1; i++) {
        int l_max = 0, r_max = 0;
        // 找右边最高的柱子
        for (int j = i; j < n; j++)
            r_max = max(r_max, height[j]);
        // 找左边最高的柱子
        for (int j = i; j >= 0; j--)
            l_max = max(l_max, height[j]);
        // 如果自己就是最高的话,
        // l_max == r_max == height[i]
81
        res += min(l_max, r_max) - height[i];
L
labuladong 已提交
82
    }
83
    return res;
L
labuladong 已提交
84 85 86 87 88 89 90
}
```

有之前的思路,这个解法应该是很直接粗暴的,时间复杂度 O(N^2),空间复杂度 O(1)。但是很明显这种计算 `r_max``l_max` 的方式非常笨拙,一般的优化方法就是备忘录。

### 二、备忘录优化

91
之前的暴力解法,不是在每个位置 `i` 都要计算 `r_max``l_max` 吗?我们直接把结果都提前计算出来,别傻不拉几的每次都遍历,这时间复杂度不就降下来了嘛。
L
labuladong 已提交
92

93
**我们开两个数组 `r_max` 和 `l_max` 充当备忘录,`l_max[i]` 表示位置 `i` 左边最高的柱子高度,`r_max[i]` 表示位置 `i` 右边最高的柱子高度**。预先把这两个数组计算好,避免重复计算:
L
labuladong 已提交
94 95 96 97 98

```cpp
int trap(vector<int>& height) {
    if (height.empty()) return 0;
    int n = height.size();
99
    int res = 0;
L
labuladong 已提交
100 101 102 103 104 105 106 107 108 109 110 111 112
    // 数组充当备忘录
    vector<int> l_max(n), r_max(n);
    // 初始化 base case
    l_max[0] = height[0];
    r_max[n - 1] = height[n - 1];
    // 从左向右计算 l_max
    for (int i = 1; i < n; i++)
        l_max[i] = max(height[i], l_max[i - 1]);
    // 从右向左计算 r_max
    for (int i = n - 2; i >= 0; i--) 
        r_max[i] = max(height[i], r_max[i + 1]);
    // 计算答案
    for (int i = 1; i < n - 1; i++) 
113 114
        res += min(l_max[i], r_max[i]) - height[i];
    return res;
L
labuladong 已提交
115 116 117
}
```

118
这个优化其实和暴力解法思路差不多,就是避免了重复计算,把时间复杂度降低为 O(N),已经是最优了,但是空间复杂度是 O(N)。下面来看一个精妙一些的解法,能够把空间复杂度降低到 O(1)。
L
labuladong 已提交
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152

### 三、双指针解法

这种解法的思路是完全相同的,但在实现手法上非常巧妙,我们这次也不要用备忘录提前计算了,而是用双指针**边走边算**,节省下空间复杂度。

首先,看一部分代码:

```cpp
int trap(vector<int>& height) {
    int n = height.size();
    int left = 0, right = n - 1;
    
    int l_max = height[0];
    int r_max = height[n - 1];
    
    while (left <= right) {
        l_max = max(l_max, height[left]);
        r_max = max(r_max, height[right]);
        left++; right--;
    }
}
```

对于这部分代码,请问 `l_max``r_max` 分别表示什么意义呢?

很容易理解,**`l_max` 是 `height[0..left]` 中最高柱子的高度,`r_max` 是 `height[right..end]` 的最高柱子的高度**

明白了这一点,直接看解法:

```cpp
int trap(vector<int>& height) {
    if (height.empty()) return 0;
    int n = height.size();
    int left = 0, right = n - 1;
153
    int res = 0;
L
labuladong 已提交
154 155 156 157 158 159 160 161
    
    int l_max = height[0];
    int r_max = height[n - 1];
    
    while (left <= right) {
        l_max = max(l_max, height[left]);
        r_max = max(r_max, height[right]);
        
162
        // res += min(l_max, r_max) - height[i]
L
labuladong 已提交
163
        if (l_max < r_max) {
164
            res += l_max - height[left];
L
labuladong 已提交
165 166
            left++; 
        } else {
167
            res += r_max - height[right];
L
labuladong 已提交
168 169 170
            right--;
        }
    }
171
    return res;
L
labuladong 已提交
172 173 174 175 176
}
```

你看,其中的核心思想和之前一模一样,换汤不换药。但是细心的读者可能会发现次解法还是有点细节差异:

177
之前的备忘录解法,`l_max[i]``r_max[i]` 分别代表 `height[0..i]``height[i..end]` 的最高柱子高度。
L
labuladong 已提交
178 179

```cpp
180
res += min(l_max[i], r_max[i]) - height[i];
L
labuladong 已提交
181 182 183 184 185 186 187 188
```

![](../pictures/%E6%8E%A5%E9%9B%A8%E6%B0%B4/3.jpg)

但是双指针解法中,`l_max``r_max` 代表的是 `height[0..left]``height[right..end]` 的最高柱子高度。比如这段代码:

```cpp
if (l_max < r_max) {
189
    res += l_max - height[left];
L
labuladong 已提交
190 191 192 193 194 195 196 197
    left++; 
} 
```

![](../pictures/%E6%8E%A5%E9%9B%A8%E6%B0%B4/4.jpg)

此时的 `l_max``left` 指针左边的最高柱子,但是 `r_max` 并不一定是 `left` 指针右边最高的柱子,这真的可以得到正确答案吗?

198
其实这个问题要这么思考,我们只在乎 `min(l_max, r_max)`**对于上图的情况,我们已经知道 `l_max < r_max` 了,至于这个 `r_max` 是不是右边最大的,不重要。重要的是 `height[i]` 能够装的水只和较低的 `l_max` 之差有关**
L
labuladong 已提交
199

L
labuladong 已提交
200 201
![](../pictures/%E6%8E%A5%E9%9B%A8%E6%B0%B4/5.jpg)

202
这样,接雨水问题就解决了。
L
labuladong 已提交
203

204
**_____________**
E
eric496 已提交
205

206
**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**
L
labuladong 已提交
207

208
**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**
L
labuladong 已提交
209

210
<p align='center'>
L
labuladong 已提交
211
<img src="../pictures/qrcode.jpg" width=200 >
212
</p>
L
labuladong 已提交
213

F
FanFan0919 已提交
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309
======其他语言代码======

[Yifan Zhang](https://github.com/FanFan0919) 提供 java 代码

**双指针解法**:时间复杂度 O(N),空间复杂度 O(1)

对cpp版本的解法有非常微小的优化。  
因为我们每次循环只会选 left 或者 right 处的柱子来计算,因此我们并不需要在每次循环中同时更新`maxLeft``maxRight`
我们可以先比较 `maxLeft``maxRight`,决定这次选择计算的柱子是 `height[left]` 或者 `height[right]` 后再更新对应的 `maxLeft``maxRight`
当然这并不会在时间上带来什么优化,只是提供一种思路。

```java
class Solution {
    public int trap(int[] height) {
        if (height == null || height.length == 0) return 0;
        int left = 0, right = height.length - 1;
        int maxLeft = height[left], maxRight = height[right];
        int res = 0;
        
        while (left < right) {
            // 比较 maxLeft 和 maxRight,决定这次计算 left 还是 right 处的柱子
            if (maxLeft < maxRight) {
                left++;
                maxLeft = Math.max(maxLeft, height[left]);  // update maxLeft
                res += maxLeft - height[left];
            } else {
                right--;
                maxRight = Math.max(maxRight, height[right]);   // update maxRight
                res += maxRight - height[right];
            }
        }
        
        return res;
    }
}
```

附上暴力解法以及备忘录解法的 java 代码

**暴力解法**:时间复杂度 O(N^2),空间复杂度 O(1)  
```java
class Solution {
    public int trap(int[] height) {
        if (height == null || height.length == 0) return 0;
        int n = height.length;
        int res = 0;
        // 跳过最左边和最右边的柱子,从第二个柱子开始
        for (int i = 1; i < n - 1; i++) {
            int maxLeft = 0, maxRight = 0;
            // 找右边最高的柱子
            for (int j = i; j < n; j++) {
                maxRight = Math.max(maxRight, height[j]);
            }
            // 找左边最高的柱子
            for (int j = i; j >= 0; j--) {
                maxLeft = Math.max(maxLeft, height[j]);
            }
            // 如果自己就是最高的话,
            // maxLeft == maxRight == height[i]
            res += Math.min(maxLeft, maxRight) - height[i];
        }
        return res;
    }
}
```

**备忘录解法**:时间复杂度 O(N),空间复杂度 O(N)
```java
class Solution {
    public int trap(int[] height) {
        if (height == null || height.length == 0) return 0;
        int n = height.length;
        int res = 0;
        // 数组充当备忘录
        int[] maxLeft = new int[n];
        int[] maxRight = new int[n];
        // 初始化 base case 
        maxLeft[0] = height[0];
        maxRight[n - 1] = height[n - 1];
        
        // 从左向右计算 maxLeft
        for (int i = 1; i < n; i++) {
            maxLeft[i] = Math.max(maxLeft[i - 1], height[i]);
        }
        // 从右向左计算 maxRight
        for (int i = n - 2; i >= 0; i--) {
            maxRight[i] = Math.max(maxRight[i + 1], height[i]);
        }
        // 计算答案
        for (int i = 1; i < n; i++) {
            res += Math.min(maxLeft[i], maxRight[i]) - height[i];
        }
        return res;
    }
}
```