刷题笔记:力扣第42题-接雨水

1.这是一道非常经典的力扣困难题,考察双指针。题意不难理解,一开始想到的思路如下为:定义左右指针从头开始遍历数组,令左边为木桶短边,一直向右寻找比这条短边更长的长边,找到后累加木桶容积。完整代码如下:

 1. int trap(int* height, int heightSize) {
 2.     int l = 0, r = l + 1;
 3.     int res = 0;
 4.     while (l < heightSize - 2){
 5.         while (r < heightSize && height[r] < height[l]) r++;
 6.         if (r == heightSize || r - l == 1){
 7.             l++;
 8.             r = l + 1;
 9.             continue;
10.         }
11.  
12.         for (int i = l + 1; i < r; i++){
13.             res += height[l] - height[i];
14.         }
15.         l = r;
16.         r = l + 1;
17.     }
18.  
19.     return res;
20. }

2.代码中还包含了一些简单的剪枝操作:剩余元素个数小于3则一定没有容积(l < heightSize - 2)、找到的长边与之前的短边只相差距离1则没有容积(r - l == 1)。

3.本地算例通过了,但是提交答案出错了,报错如下:

可以看到对于这个数组,唯一的木桶短边在右侧,而我的代码默认了“右边界一定要比左边界高(或等高)”,所以一定会漏掉这种情况导致输出容积为0,说明当前的方法考虑不周全。

4.尝试另一种方法,先假定左边为木桶的一边,然后去寻找凹槽,即第一个比该边短的长度,之后再去寻找第一个比该凹槽短边长的边为木桶另一边,比较木桶两边中的短边为实际边长,计算容积。完整代码如下:

 1. int trap(int* height, int heightSize) {
 2.     if (heightSize < 3) return 0;
 3.  
 4.     int l = 0, r = l + 1;
 5.     int res = 0;
 6.     while (l < heightSize - 2){
 7.         while (r < heightSize && height[r] >= height[l]){
 8.             l++;
 9.             r++;
10.             if (l == heightSize - 2) return 0;
11.         }
12.  
13.         int tmp = height[r];
14.         while (r < heightSize && height[r] <= tmp) r++;
15.         if (r == heightSize){
16.             l++;
17.             r = l + 1;
18.             continue;
19.         }
20.  
21.         int len = fmin(height[l], height[r]);
22.         for (int i = l + 1; i < r; i++){
23.             res += len - height[i];
24.         }
25.         l = r;
26.         r = l + 1;
27.     }
28.  
29.     return res;
30. }

5.这次连第二个本地算例都没有通过,报错如下:

对于这个数组,如果用我的代码,右边找到3时就会停下计算容积,但实际上的右边应该为5,导致容积计算错误。这是因为我的代码用的是“局部贪心”思想,找到一个凹槽就累加到容积中,但忽视了“大桶套小桶”的情况导致漏算容积。

6.本题的正确做法是从“横向找容器”转变为“纵向找水柱”,不再去管局部容器的左右边界,反而去关注“第i根柱子上能放多少水”。以下是完整答案:

 1. int trap(int* height, int heightSize) {
 2.     // 少于3根柱子无法形成积水,直接返回0
 3.     if (heightSize < 3) return 0;
 4.  
 5.     // l左指针,r右指针
 6.     int l = 0, r = heightSize - 1;
 7.     // l_max:左侧最高柱子高度,r_max:右侧最高柱子高度
 8.     int l_max = 0, r_max = 0;
 9.     // 累计雨水总量
10.     int res = 0;
11.     
12.     while (l < r){
13.         // 更新左右两侧最高高度
14.         l_max = fmax(l_max, height[l]);
15.         r_max = fmax(r_max, height[r]);
16.  
17.         // 左侧柱子更矮,积水由左侧最高墙决定
18.         if (height[l] < height[r]){
19.             res += l_max - height[l];
20.             l++;
21.         } else {
22.             // 右侧柱子更矮,积水由右侧最高墙决定
23.             res += r_max - height[r];
24.             r--;
25.         }
26.     }
27.  
28.     return res;
29. }

该算法时间复杂度为O(n),空间复杂度为O(1)。

7.为什么官方题解中说“如果height[l] < height[r],则必有l_max < r_max”?对于height[l] < height[r],此时有四种情况:

①height[l] == l_max,height[r] == r_max;

②height[l] < l_max,height[r] == r_max;

③height[l] == l_max,height[r] < r_max;

④height[l] < l_max,height[r] < r_max;

对于①③两种情况很容易得出证明,主要看②和④。这其中涉及到该算法中双指针移动的底层逻辑,即“谁小谁移动”。为什么现在height[l]小于l_max了?说明l移动了,而移动的原因便是在右边出现了一个比l_max更大的元素x,这个元素x小于等于r_max,所以可得height[l] < l_max < x <= r_max,也就可以直接证明出l_max < r_max,且证明过程实际上与height[r]无关。对于“如果height[l] >= height[r],则必有l_max >= r_max”的证明也是如此。

8.该算法的实际过程类似于我的第一个“横向找短板”的双向版本,我的代码只考虑了从左向右这一单边方向,而实际上应该使用左右双向,将这个数组分成左右两个区间来分别计算容积,最后左右指针汇聚到最高的柱子处。比较height[l]和height[r]的大小可以保证永远是矮的那一边来计算容积,它的另一边一定是更高的。

9.官方还给出了动态规划的解法,但我看官方这张图,这难道不是小学奥数的“容斥”问题吗?

先从左向右遍历,得到完整面积中的右边;再从右向左遍历,得到完整面积的左边;最后遍历得出完整面积,由于每块独立面积都包含最后的容积面积,所以两块独立面积相加后再减去完整面积就是容积。完整代码如下:

 1. int trap(int* height, int heightSize) {
 2.     // 柱子数量小于3无法蓄水,直接返回0
 3.     if (heightSize < 3) return 0;
 4.  
 5.     // l_max:从左往右遍历记录的全局最大高度;r_max:从右往左遍历记录的全局最大高度
 6.     int l_max = 0, r_max = 0;
 7.     
 8.     // resl:只以左侧最高墙计算,每个位置能存的水量总和(不考虑右侧墙体限制)
 9.     int resl = 0;
10.     for (int i = 0; i < heightSize; i++){
11.         l_max = fmax(l_max, height[i]);
12.         // 当前柱子低于左侧最高墙,则可以存水
13.         if (height[i] < l_max){
14.             resl += l_max - height[i];
15.         }
16.     }
17.  
18.     // resr:只以右侧最高墙计算,每个位置能存的水量总和(不考虑左侧墙体限制)
19.     int resr = 0;
20.     for (int i = heightSize - 1; i >= 0; i--){
21.         r_max = fmax(r_max, height[i]);
22.         if (height[i] < r_max){
23.             resr += r_max - height[i];
24.         }
25.     }
26.  
27.     // 单独用全局左最大高度算出的总水量,是每个位置左墙高度减柱子高度
28.     int res = 0;
29.     for (int i = 0; i < heightSize; i++){
30.         res += l_max - height[i];
31.     }
32.     // 容斥原理:真实蓄水量 = 左单边累加 + 右单边累加 - 全局高墙单边累加
33.     res = resl + resr - res;
34.  
35.     return res;
36. }

该算法时间复杂度为O(n),空间复杂度为O(1)。

10.而官方的解法是在两次单向遍历时分别记录两种方向上水柱的最大值,之后用两个最大值中的最小值来减去柱子的高度得出实际上的水柱高度,本质上也是在以最大柱子为界限的左右边界上做文章。

该题目确实经典,值得之后多次回顾。官方题解中还有单调栈的解法,后面学到单调栈的时候再回来看看。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值