接雨水-动态规划解法记录
题目描述:给定 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
思路:黑色的看成墙,蓝色的看成水,宽度一样,给定一个数组,每个数代表从左到右墙的高度,求出能装多少单位的水。也就是图中蓝色正方形的个数。
解法一:按行求
整个思路就是,求第 i 层的水,遍历每个位置,如果当前的高度小于 i,并且两边有高度大于等于 i 的,说明这个地方一定有水,水就可以加 1。
如果求高度为 i 的水,首先用一个变量 temp 保存当前累积的水,初始化为 0。从左到右遍历墙的高度,遇到高度大于等于 i 的时候,开始更新 temp。更新原则是遇到高度小于 i 的就把 temp 加 1,遇到高度大于等于 i 的,就把 temp 加到最终的答案 ans 里,并且 temp 置零,然后继续循环。
我们就以题目的例子讲一下。先求第 1 行的水。
也就是红色区域中的水,数组是 height = [ 0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1 ] 。
原则是高度小于 1,temp ++,高度大于等于 1,ans = ans + temp,temp = 0。
temp 初始化为 0,ans = 0
height[0] 等于 0 < 1,不更新。
height[1] 等于 1 >= 1,开始更新 temp。
height[2] 等于 0 < 1,temp = temp + 1 = 1。
height[3] 等于 2 >= 1,ans = ans + temp = 1,temp = 0。
height[4] 等于 1 >= 1,ans = ans + temp = 1,temp = 0。
height[5] 等于 0 < 1,temp = temp + 1 = 1。
height[6] 等于 1 >= 1,ans = ans + temp = 2,temp = 0。
剩下的 height[7] 到最后,高度都大于等于 1,更新 ans = ans + temp = 2,temp = 0。而其实 temp 一直都是 0,所以 ans 没有变化。
再求第 2 行的水。
也就是红色区域中的水,数组是 height = [ 0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1 ]。
原则是高度小于 2,temp ++,高度大于等于 2,ans = ans + temp,temp = 0。
temp 初始化为 0,ans 此时等于 2。
height[0] 等于 0 < 2,不更新。
height[1] 等于 1 < 2,不更新。
height[2] 等于 0 < 2,不更新。
height[3] 等于 2 >= 2,开始更新
height[4] 等于 1 < 2,temp = temp + 1 = 1。
height[5] 等于 0 < 2,temp = temp + 1 = 2。
height[6] 等于 1 < 2,temp = temp + 1 = 3。
height[7] 等于 3 >= 2,ans = ans + temp = 5,temp = 0。
height[8] 等于 2 >= 2,ans = ans + temp = 5,temp = 0。
height[9] 等于 1 < 2,temp = temp + 1 = 1。
height[10] 等于 2 >= 2,ans = ans + temp = 6,temp = 0。
height[11] 等于 1 < 2,temp = temp + 1 = 1。
然后结束循环,此时的 ans 就是6。
再看第 3 层。
按照之前的算法,之前的都是小于 3 的,不更新 temp,然后到 height[7] 等于 3,开始更新 temp,但是后边没有 height 大于等于 3 了,所以 ans 没有更新。
所以最终的 ans 就是 6。
1 | public int trap(int[] height) { |
时间复杂度:如果最大的数是 m,个数是 n,那么就是 O(m∗n)。
空间复杂度:O(1)。
这种解法的性能对于m即数组数据很敏感,加入一个数组[1,0,99999],它会遍历99999行,对于相对平缓的数组比较友好。
解法二:按列求
求每一列的水,我们只需要关注当前列,以及左边最高的墙,右边最高的墙就够了。
装水的多少,当然根据木桶效应,我们只需要看左边最高的墙和右边最高的墙中较矮的一个就够了。
所以,根据较矮的那个墙和当前列的墙的高度可以分为三种情况。
较矮的墙的高度大于当前列的墙的高度
把正在求的列左边最高的墙和右边最高的墙确定后,然后为了方便理解,我们把无关的墙去掉。
这样就很清楚了,现在想象一下,往两边最高的墙之间注水。正在求的列会有多少水?
很明显,较矮的一边,也就是左边的墙的高度,减去当前列的高度就可以了,也就是 2 - 1 = 1,可以存一个单位的水。
较矮的墙的高度小于当前列的墙的高度
同样的,我们把其他无关的列去掉。
想象下,往两边最高的墙之间注水。正在求的列会有多少水?
正在求的列不会有水,因为它大于了两边较矮的墙。
较矮的墙的高度等于当前列的墙的高度。
和上一种情况是一样的,不会有水。
明白了这三种情况,程序就很好写了,遍历每一列,然后分别求出这一列两边最高的墙。找出较矮的一端,和当前列的高度比较,结果就是上边的三种情况。
1 | public int trap(int[] height) { |
时间复杂度:O(n^2^),遍历每一列需要n,找出左边最高和右边最高的墙加起来刚好又是一个n,所以是n^2^。
空间复杂度:O(1)。
解法三: 动态规划
我们注意到,解法二中。对于每一列,我们求它左边最高的墙和右边最高的墙,都是重新遍历一遍所有高度,这里我们可以优化一下。
首先用两个数组,max_left[i]
代表第 i
列左边最高的墙的高度,max_right[i]
代表第 i
列右边最高的墙的高度。(一定要注意下,第 i 列左(右)边最高的墙,是不包括自身的,和 leetcode 上边的讲的有些不同)
对于 max_left
我们其实可以这样求。
max_left[i] = Max(max_left[i-1],height[i-1])
。它前边的墙的左边的最高高度和它前边的墙的高度选一个较大的,就是当前列左边最高的墙了。
对于 max_right
我们可以这样求。
max_right[i] = Max(max_right[i+1],height[i+1])
。它后边的墙的右边的最高高度和它后边的墙的高度选一个较大的,就是当前列右边最高的墙了。
这样,我们再利用解法二的算法,就不用在 for
循环里每次重新遍历一次求 max_left
和 max_right
了。
1 | public int trap(int[] height) { |
时间复杂度:O(n)。
空间复杂度:O(n),用来保存每一列左边最高的墙和右边最高的墙。
解法四:双指针(待完善)
动态规划中,我们常常可以对空间复杂度进行进一步的优化。
例如这道题中,可以看到,max_left[i] 和 max_right[i] 数组中的元素我们其实只用一次,然后就再也不会用到了。所以我们可以不用数组,只用一个元素就行了。我们先改造下 max_left。
1 | public int trap(int[] height) { |
我们成功将 max_left 数组去掉了。但是会发现我们不能同时把 max_right 的数组去掉,因为最后的 for 循环是从左到右遍历的,而 max_right 的更新是从右向左的。