继续刷LeetCode 热题 HOT 100 的题目,并且在博客更新我的solutions。在csdn博客中我会尽量用文字解释清楚,相关Java代码大家可以前往我的个人博客jinhuaiyu.com中查看。
今天的题目比较难,主要是数学思维要清晰我感觉自己就是个数学渣渣……连计算面积都不会。今天又是*看官方解答的一天,一共有三种比较好的解答方法,其中有两种统计面积的数学方法。
题目:接雨水
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
示例 1:
输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。
示例 2:
输入:height = [4,2,0,3,2,5]
输出:9
提示:
n == height.length
1 <= n <= 2 * 104
0 <= height[i] <= 105
solution 1:动态规划
这种方法里涉及的雨量统计方法以及算法思想最简单和直观,因此我们先从这个方法开始。
首先介绍第一种统计雨量的方法,我们根据示例1的图可以看出,每个柱子上面有可能有雨也可能没有雨,如果有雨,一定是左右两侧都有比它高的柱子(注意,我不是说一根柱子能接雨,而是说在整个环境中,这个柱子上面有雨,后面提到柱子接雨都是这个意思)。假设从这个柱子左边最高的柱子高leftMax,右边最高的高rightMax,那么这个柱子上面的雨的高度则取决于leftMax和rightMax中较短的那个的高度减去这个柱子的高度,比如三根柱子高分别为314,则可以在中间那根柱子上接雨的高度为3-1=2。
如果柱子上没有雨,说明两侧一定有至少一侧没有比它高的柱子。为了将这种情况和前面的情况合并,我们可以令leftMax和rightMax统计时都包含当前这根柱子。这样的话,可以得到每根柱子上面接雨量统一计算公式:
Math.min(leftMax[i], rightMax[i]) - height[i]
如果当前柱子最高,则leftMax[i]= rightMax[i]=height[i]。
动态规划的思想是保存每次计算的结果供下一次计算继续使用。我们可以设置int[] leftMax和int[] rightMax两个数组来存放每个位置左右两侧中(包含自身)最高柱子的高度。通过两次循环遍历数组height可以将两个数组填充上,供后面使用,填充方法如下:
leftMax[i] = Math.max(leftMax[i-1], height[i]);
rightMax[i] = Math.max(rightMax[i + 1], height[i]);
最后,我们从左往右遍历height[],可以轻松得到每个柱子上面接雨量,并将它们加起来得到总的接雨量。
solution 2:双指针法
动态规划的空间复杂度是 O(n),我们要先通过两次遍历把leftMax[]和rightMax[]两个数组填充上,最后再遍历一轮计算每个柱子接雨量。有没有一种方法,能够一轮遍历就计算完每个柱子的接雨量,且不用数组来计算和维护每个位置左右两侧最高柱子的高度,将空间复杂度降到 O(1)?
我们用左右指针,同时从两端开始往中间遍历,不再维护每一个位置的左右两侧最高柱子的高度,而是只维护当前遍历到的。用变量leftMax计算left指针指向的柱子左侧最高柱子高度,rightMax变量计算right指针指向的柱子右侧最高柱子高度。左右两个指针中只有较小的那个才会往中间移动,如果left柱子较小,那么right柱子一定比leftMax高,两个指针相遇时那个柱子一定是最高的柱子,最高柱子接不了雨。
我们以当前left比right柱子矮为例。上一轮循环时,较小的柱子的指针往中间移动了一位,我们可以轻松计算并更新leftMax和rightMax变量,它们分别代表当前left指向的柱子左侧(包括自身)最高的柱子高度和right指向的柱子右侧(包括自身)最高的柱子高度。因为此时height[left] < height[right],尽管此时height[right]不一定是left柱子右侧最高高度,但它绝对比leftMax大。不然如果它比leftMax小,之前应该往里移动的就是它了,而right指针还指向它,形成悖论,可反证得上述结论是正确的。决定一个柱子上面接雨量的是左右两侧最高柱子中较小的那个,所以是leftMax。
此时有leftMax<height[right]<=left指向的柱子右侧最高柱子的高度(通过不等式我们可以发现这个高度我们不用再计算了,因为我们只要求这三者中最小的)。
left不比right矮的情况同理。
注意,我们一直只维护左指针左侧最高柱子高度和右指针右侧最高柱子高度,另一侧的高度我们并不需要知道,因为我们每次计算和移动的都是左右指针中较矮的那一个,所以另一侧一定不会比我们维护的那一侧矮。左右指针是趋向中间最高的那个柱子移动的,自然靠中间那侧永远更高。
solution 3:单调递减栈
这种方法使用的是另一种雨量统计方法,举个简单例子,32123,321三根柱子还不能接雨,当把第四根高为2的柱子加入时,212可以接一个单位的雨,我们把这个数值加入总接雨量,然后想象把它填平,即变成了3222,此时不能接雨。加入第五根高位3的柱子时,又能接3个单位的雨。这三个单位的雨在之前那一个单位的雨的上层。
之前的雨量统计方法是统计每个柱子上的雨,这种统计方法是按照高度一层一层统计。
但是把这种思想转化成算法还有很多困难的地方,首先,我们要利用到单调递减栈(就是下层元素不能比上层的大),存的是柱子的位置。
从左往右开始遍历每一根柱子,如果当前柱子比栈顶柱子小或一样高,就入栈。如果比栈顶柱子大,说明可以和栈顶下面一个位置的柱子(如果有的话)包围住栈顶这个位置接水,这两个柱子接的水的高度取决于其中较低的那个减掉栈顶柱子高度,如栈内已有21两个高度的柱子,当前遍历到柱子高度为3,则高度为2和3的柱子可接水:(2-1)*宽度,宽度为2和3高度两个柱子间距离。弹出top,把这部分接水量计入总接水量(想象成填平一个坑),此时继续判断栈顶柱子和栈顶下一个位置的柱子。
如果还能和当前遍历到的柱子组成一个接水位置,就继续进行上述操作(填平小坑上的大坑)。如栈内有321三个高度柱子,当前遍历到4,4和21组成一个只有一个单位高度的接水位置,计入总数,弹出1(填平这个位置的坑);然后4还可以和3组成一个单位高度的接水位置,但宽度为2(一个小坑上的大坑)。
当然,我们每次要先弹出栈顶才能得到栈下一个位置的柱子,如果弹出栈顶后栈空了,就不存在能和当前遍历的柱子组成接水的柱子了。
如果当前栈顶柱子比当前遍历到的柱子高或一样高,或者栈空,则当前柱子入递减栈。
这种用入栈出栈来模拟不断往右侧加柱子并且填平这根柱子导致的坑,保持栈一直是非递增的,填空的过程就是计算接雨量的过程。
Finally,带有详细注释的代码放在我的个人博客http://jinhuaiyu.com/leetcode-trapping-rain-water/