LeetCode7 - 接雨水
📝 题目描述
题目链接:接雨水
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
示例:
1 |
|
提示:
n == height.length1 <= n <= 2 * 10^40 <= height[i] <= 10^5
💡 解题思路
方法一:动态规划
我们先考虑一般情况,特殊情况我们最后再考虑。
①一般情况:
比如,对于上图中的横坐标为2的位置,它在垂直方向能接多少雨水呢?
我们可以得到对于每个横坐标位置,接雨水的量 = 该位置雨水可以淹没多高 - 该位置的高度;
很明显该位置的高度就是height[2] = 1,那么怎么算该位置可以雨水可以淹没多高呢?
这有点像“LeetCode5 - 盛最多水的容器”,比如对于横坐标为2的位置,我们向左看,最高的柱子是横坐标为1的位置(设为leftMax[2] = height[1] = 3),向右看,最高的柱子是横坐标为6的位置(设为rightMax[2] = height[6] = 3);
这样一来,横坐标为2的位置雨水就能淹没min(leftMax[2],rightMax[2])也就是3的高度了。
可以得出横坐标为2的位置接雨水的量 = 3 - 1 = 2。
②特殊情况:
特殊情况其实也没什么特殊的,就是边界处,对于上图横坐标为0的位置,它再向左没有柱子了,那么我们可以设leftMax[0]=height[0],同样我们可以设rightMax[8]=height[8]。
规范一些,定义:leftMax[i] 表示下标 i 及其左边的位置中,height 的最大高度(就是同时也看自身的高度);同理,rightMax[i] 表示下标 i 及其右边的位置中,height 的最大高度。
通过上面的分析,暴力解法呼之欲出了,也就是我们遍历每一个高度,计算它能接到的垂直方向雨水量,然后累加起来。这样的话,对于每个高度我们都要遍历一次整个数组来找到leftMax[i],rightMax[i],复杂度达到了 。
思考一下,我们发现,其实上面的过程做了很多重复的工作,能不能提前扫描数组,将leftMax[i],rightMax[i]预置好呢?
假设我们要填leftMax数组,也就是“下标 i 及其左边的位置中,height 的最大高度”,想一想,对于位置0,那么leftMax[0] = height[0]没得选;对于位置1,我们也不必重新遍历1前面的数了,直接对比leftMax[0]和height[1],谁大leftMax[1]就是谁。
也就是说,我们从左往右填leftMax数组,那么leftMax[i]=max(leftMax[i−1],height[i]);
同样,假设我们要填rightMax数组,也就是“下标 i 及其右边的位置中,height 的最大高度”,对于位置n-1,那么rightMax[n-1] = height[n-1];对于其他位置i,对比rightMax[0]和height[i],谁大rightMax[k]就是谁。
可以得出,我们从右往左填rightMax数组,rightMax[i]=max(rightMax[i+1],height[i])。
做完这些,那么剩下的只需要从头遍历一下height数组,然后得到min(leftMax[i],rightMax[i]),再减去height[i],累加一下啊,就得到了正确答案。
方法二:单调栈
我们维护一个单调栈,单调栈存储的是下标,满足从栈底到栈顶的下标对应的数组 height 中的元素递减,并从左往右遍历height[i]:
- 如果栈为空,直接进栈;否则判断栈顶元素
top对应的height[top]与height[i]的关系:- 如果
height[top] > height[i]:我们可以想象,栈中的元素对应的height单调递减,那么直到height[i]都是递减的,无法形成低洼接到雨水,于是将i进栈; - 如果
height[top] < height[i]:- 如果栈内只有一个元素,那么
height[top]前的元素一定因为比height[top]还要低才不在栈中,无法形成低洼,因此直接将top出栈,将i进栈。 - 如果栈内至少有两个元素,记
top的下面一个元素是left,则由单调性可知height[left] > height[top] < height[i], 可以形成一个接雨水的区域,该区域的宽度是i − left − 1,高度是min(height[left], height[i]) − height[top],为了得到left,需要将top出栈。在对top计算能接的雨水量之后,left变成新的top。
2、重复上述操作。
- 如果栈内只有一个元素,那么
- 如果
方法三:双指针
想一下刚才的动态规划,需要维护两个数组 leftMax 和 rightMax,因此空间复杂度是 O(N)。是否可以将空间复杂度降到 O(1)?
注意到下标 i 处能接的雨水量由 leftMax[i] 和 rightMax[i] 中的最小值决定。由于数组 leftMax 是从左往右计算,数组 rightMax 是从右往左计算,因此可以使用双指针和两个变量代替两个数组。
维护两个指针 left 和 right,以及两个变量 leftMax 和 rightMax,初始时 left=0,right=n−1,leftMax=0,rightMax=0。指针 left 只会向右移动,指针 right 只会向左移动,在移动指针的过程中维护两个变量 leftMax 和 rightMax 的值。
当两个指针没有相遇时,进行如下操作:
- 使用
height[left]和height[right]的值更新leftMax和rightMax的值; - 如果
height[left] < height[right],则必有leftMax < rightMax,下标left处能接的雨水量等于leftMax − height[left],将下标left处能接的雨水量加到能接的雨水总量,然后将left加1(即向右移动一位); - 如果
height[left] ≥ height[right],则必有leftMax ≥ rightMax,下标right处能接的雨水量等于rightMax − height[right],将下标right处能接的雨水量加到能接的雨水总量,然后将right减1(即向左移动一位)。
当两个指针相遇时,即可得到能接的雨水总量。
为什么“如果 height[left] < height[right],则必有 leftMax < rightMax”?
首先我们要知道,算法的逻辑是“谁小移动谁”,即 height[left] < height[right] 时移动 left,否则移动 right;
假如当前状态是 height[left] < height[right],
如果 leftMax 很大(比 rightMax 大):说明我们在左边曾经遇到过一个非常高的柱子(比如高度为 100)。 而在那个时刻(左指针指向 100 时),右边的 rightMax 还很小(比如只有 5)。 根据移动规则:height[left] (100) > height[right] (<=5)。 那时候算法会强迫移动右指针,而左指针会停在那个 100 的位置不动。
左指针什么时候才能动?只有当右指针不断向左移动,并且遇到了一个比 100 还要高的柱子(更新了 rightMax > 100),此时 height[right] > height[left] 了,左指针才会被允许移动。
因此只要左指针在动(说明 height[left] < height[right]),就意味着右边一定已经存在一个比 leftMax 更高的挡板(rightMax)了,否则左指针早就被卡住不动了。
🔧 代码实现
1、动态规划
1 | class Solution { |
2、单调栈
1 | class Solution { |
3、双指针
1 | class Solution { |
📊 复杂度分析
1、动态规划
- 时间复杂度:遍历三次,时间复杂度为 。
- 空间复杂度:需要额外的数组存储
leftMax[i],rightMax[i],空间占用为 。
2、单调栈
- 时间复杂度:遍历一次,时间复杂度为 。
- 空间复杂度:需要额外的数组存储栈,空间占用为 。
2、双指针
- 时间复杂度:遍历一次,时间复杂度为 。
- 空间复杂度:无需额外占用和输入规模相关的存储空间为 。
🎯 总结
- 核心思想:想到朴素算法,一步步优化。