题目
给定 n 个非负整数表示每个宽度为 1 的柱子的高度数组height,计算按此排列的柱子,下雨之后能接多少雨水。
题解
解法1:动态规划,O(n)空间复杂度
拆分问题
所有的积水由每一列的积水量累加得来。
某一列的积水量由什么决定?
- 先要看左右两边最高的柱子有多高,这决定了当前列能积多高的水,并且最多只能积到较矮的那个柱子的高度,否则水会溢出。
- 这个高度减去当前柱子高度,就是当前列可以积的水量。
第i列的积水量求解公式
第i列的积水量 = min(0到i - 1列中最高的柱子的高度, i + 1列到n - 1列中最高柱子的高度) - 当前列柱子高度
所以要把每一列左边和右边最高的柱子先求出来。
第i列左边的最高的柱子怎么找?
从0到i - 1遍历一遍height数组,找最大的。
实际可以从左到右遍历一遍height数组,把前面记录的最大的柱子高度跟第i - 1个柱子高度比较,取较大值就行了。
第i列右边的最高的柱子的高度怎么找?
从右向左遍历一遍height数组,把前面记录的最大的柱子高度跟第i + 1个柱子高度做对比,取较大的。
边界情况
第1个柱子左边没有柱子,其左边最高柱子高度是0,也无法积水。
最后1个柱子右边没有柱子,其右边最高柱子高度是0,也无法积水。
代码(kotlin)
1 | class Solution { |
时间复杂度: O(n)
空间复杂度: O(n)
解法2:动态规划,O(1)空间复杂度
空间优化
求每一列的积水的时候,maxLeft[i]
和maxRight[i]
只用到了一次,后续不会再查询之前的值,所以可以用两个变量leftMax
、rightMax
代替数组。
怎么用leftMax代替maxLeft数组?
maxLeft
数组是从左到右遍历数组height求得的,在从左到右遍历height数组求第i列积水量的时候,就可以顺便计算出第i个柱子左边最高的柱子的高度,即leftMax
。
怎么用rightMax代替maxRight数组?
maxRight
数组是从右到左遍历数组height求得的,想办法能想办法能从右到左遍历数组就行了。
什么时候应该从右到左遍历?什么时候从左到右?
当左侧的柱子高度比右侧柱子高,需要看较矮的右侧的柱子高度,才能决定某一列能积多少水,左边不管多高都不会决定能积多少水,此时应该从右到左遍历。
直到右侧柱子比左侧柱子高,才需要从左边到右边检查。
初始情况是怎样的?怎么开始?
第一列左边没有柱子,无法积水。
最后一列右边没有柱子,无法积水。
计算积水只考虑区间[1, n - 2]
。
可以直接令:leftMax = height[0]
rightMax = height[n - 1]
具体怎么判断决定height数组的遍历方向?
- 如果发现左边柱子比右边矮,即
leftMax < rightMax
,可以先计算左边的积水,随后如果发现当前柱子高度比leftMax高,再更新leftMax。 - 如果发现右边柱子比左边矮,即
leftMax > rightMax
,先计算右边的积水,随后如果发现当前柱子高度比rightMax高,再更新rightMax。
代码
1 | class Solution { |
时间复杂度: O(n)
空间复杂度: O(1)
解法3:直观模拟 -> 单调栈
梳理
要能积水,需要多根柱子形成凹槽。
如何判断形成了凹槽?
朝一个方向依次看柱子高低,是否能形成高、低、高的排列。
在已经形成下坡的情况下,一旦发现出现了上坡,就形成了凹槽,就可以计算积水量了。
如果发现上坡,但是前面没有下坡,无法积水。
如何计算凹槽的积水量
穷举出凹槽的所有可能情况,逐个分析,再归纳。
从最基本的情况看起,再添加有限的步骤,推导出更复杂的情况。
基本情况
一个凹槽最少也要3个柱子,假设3个柱子从左到右依次叫L、M、R,高度排列依次是高、低、高。凹槽积水量 = minOf(L高度, R高度) - M高度
如果有多个相同高度的M,计算方式会怎么变化?
就变成求矩形面积了:凹槽积水量 = (minOf(L高度, R高度) - M高度) * L到R的距离
L到R的距离 = R索引 - L索引 - 1
如果有4个不同高度的柱子(多一个柱子)形成的凹槽,计算方式会怎么变化?
因为是从左到右看柱子高度的,一旦发现上坡就触发计算,所以可以先看下坡中柱子多的情况,比如:[3,2,1,4]。
1 | # |
此时积水面积不是矩形,左下方是一个阶梯型。
如果按之前的办法求解,从3依次回看2、1,会检测到凹槽形成,可以求得c的面积。
积水面积剩下a、b,可以发现刚好是一个矩形,面积为(minOf(3高度, 2高度) - 1高度) * 1到3的距离
。
如果左边还有更高的柱子,会形成新的矩形,还是一样的计算过程。
计算规律
这种阶梯形状的积水面积,可以按行拆分为不同的矩形,先求下面的矩形面积,再求上面的矩形面积,最后累加。
然后再看右边柱子多的情况,得看右边形成新的凹槽才有讨论意义,那就是看一下有两个凹槽的情况会怎样。
有两个凹槽时,计算方式会怎样变化?
1 | # |
可以发现0到3的第一个凹槽中第0个最高的柱子会影响到4到6的第二个凹槽的积水面积的计算。
第一个凹槽积水量计算过后,1和2两个柱子对于第二个凹槽积水面积计算没有影响了。
在计算新凹槽积水量的时候,需要读取以前比较高的柱子高度,太低的柱子就可以忽略了。
怎么获取和存储计算积水量需要的信息?
每个矩形面积的计算,是在发现有上坡后开始的,检测上坡就是需要知道当前柱子高度和前一个柱子高度,这个在遍历height数组时可以直接获取。
发现上坡后,需要知道:
- 前面有没有形成下坡的柱子
- 柱子之间的距离
- 柱子的高度
柱子距离必须要记录柱子索引位置;
形成下坡意思就是柱子高度要单调递减;
柱子高度可以根据柱子索引查询height数组;
访问下坡中柱子还要从右到左依次访问。
我们遍历检查的顺序是从左到右,访问顺序反过来,符合这个特点的数据结构就是栈。
入栈时保存的是柱子索引。
栈内柱子保持高度单调递减,不存储递增的柱子,因为对于后续计算积水没有意义。
当前柱子前面一个柱子也算下坡中,也存储在栈中,方便统一处理。
判断前面有没有形成下坡,就判断栈是否不为空。
代码(kotlin)
1 | import java.util.* |
解法4:数学拆解 -> O(1)空间复杂度
从图形上看积水区域特点
1 | b b b b b # a a # c c c c c |
观察得知:区间矩形面积 = a的面积 + #的面积 + b的面积 + c的面积
区间矩形面积已知。区间矩形面积 = 区间宽度 * 最高柱子高度
#的面积已知。#的面积 = 所有柱子高度的和
有没有办法知道b和c的面积?
b和c是阶梯状的,不好单独计算。
可以看其处于哪个好计算的部分,再看能不能把b和c的面积拆解出来。
- 如果从左到右遍历height数组,是可以知道
#的面积 + a的面积 + c的面积
,遇到比之前高的柱子就一直累加高柱子的高度就行了,假设这个面积为left。 - 如果从右到左遍历height数组,是可以知道
#的面积 + a的面积 + b的面积
,也是遇到比之前高的柱子就一直累加高柱子的高度就行了,假设这个面积为right。
left + right = #的面积 + a的面积 + (#的面积 + a的面积 + b的面积 + c的面积)
化简得:left + right = #的面积 + a的面积 + 区间矩形面积
可得:a的面积 = left + right - #的面积 - 区间矩形面积
1 | class Solution { |