当青训营遇上码上掘金。 这题类似LeetCode的接雨水,主要就算统计累积每一列的蓄豆量,主要有以下三种解法
1.双指针
一列能接多少青豆,取决于该节点左边最高的柱子和右边最高的柱子的最短的一根,且不能高过它 即这一列蓄豆量取决于两边最高的柱子较矮的一边 ➖ 该节点的高度 例如第一个高度为0的节点,蓄豆量h = Math.min(maxRight, maxLeft) - nowHeight = 4 - 0
注意: 第一个节点和最后一个节点是没有高度的
这样的解法有很多重复计算,在寻找最高的柱子的时候,重复了很多次,时间复杂度为O(n2),空间复杂度为O(1) 所以需要通过动态规划来保存每个节点其左右最高柱子的长度,同时配合双指针 或者双指针配合单调栈,也可以完成时间复杂度O(n),空间复杂度O(1)
public int trap(int[] height) {
int res = 0;
int len = height.length;
for (int i = 1; i < len - 1; i++){
int leftMax = height[0];
int rightMax = height[len - 1];
for (int j = 0; j < i; j++){
leftMax = Math.max(leftMax, height[j]);
}
for (int j = len - 1; j > i; j--){
rightMax = Math.max(rightMax, height[j]);
}
int h = Math.min(rightMax, leftMax) - height[i];
if (h > 0)
res += h;
}
return res;
}
将动态规划结合双指针,注意此时的双指针意义和之前的不一样了 双指针左右指针指向的是要处理的节点,再通过两个值来存储左右最大值
一个节点是否能蓄豆的条件为:左右都存在比它高的
对于左指针:
如果maxLeft < maxRight,说明右边肯定有比当前节点高的,那么怎么确定左边也有比当前节点高的 注意看maxLeft < maxRight后执行的res += maxLeft - nowHeight 如果左边没有比当前节点高的柱子,那就是maxLeft == nowHeight,即res加的是0
对于右指针:
同理,如果左边的最大值比当前指向的right指针的最大值大,说明左边肯定有比它高的,右边要么有比它大的,要么它就是最大的 说明右指针指向的节点可以更新,那么更新的值一定是比较小的maxRight - nowHeight
总结:
左右指针交替指向的节点,都能保证其指向的一边可以蓄豆,即交替保证被指向的节点其左右都有大于它的,如果相等则加0,不影响
maxLeft < maxRight <==> height[left] < height[right]怎么理解:
跟上面说的一样,如果height[left] < height[right] 说明left发现右边有比它大的了,那么为什么不再判断Math.min(maxLeft, maxRight)而是直接确定maxLeft更小呢? 这是因为如果左边真的有一个很高的节点的话,那么移动的一定是right指针,因为左边都那么高了,一直比右边高,肯定会一直更新右边的节点蓄豆量,直到右边有节点比左边高为止
但是还是maxLeft < maxRight容易理解些
public int trap(int[] height) {
int len = height.length;
int res = 0;
int left = 0;
int right = len - 1;
int maxLeft = 0;
int maxRight = 0;
while (left < right){
maxLeft = Math.max(maxLeft, height[left]);
maxRight = Math.max(maxRight, height[right]);
if (maxLeft < maxRight){
res += maxLeft - height[left++];
}else {
res += maxRight - height[right--];
}
}
return res;
}
2.动态规划
由于双指针遍历的时候有很多重复计算的节点,所以动态规划通过2个数组来记录其左右最高柱子的长度
由于记录了每个节点的状态,所以求解只需要两轮遍历即可,时间复杂度和空间复杂度都是O(n)
public int trap(int[] height) {
int len = height.length;
int[] maxLeft = new int[len];
maxLeft[0] = height[0];
int[] maxRight = new int[len];
maxRight[len - 1] = height[len - 1];
for (int i = 1,j = len - 2; i < len && j >= 0; i++,j--){
maxLeft[i] = Math.max(maxLeft[i - 1], height[i]);
maxRight[j] = Math.max(maxRight[j + 1], height[j]);
}
int res = 0;
for (int i = 1; i < len - 1; i++){
int h = Math.min(maxRight[i], maxLeft[i]) - height[i];
res += Math.max(h, 0);
}
return res;
}
3.单调栈
每一个节点入栈
- 如果比前一个节点高度低,就直接入栈
- 如果高度相同,则将栈中元素替换成当前的,因为当前元素和前一个元素中间不可能蓄水了,前一个元素的顶部可以蓄水,不过每次都有保留其更前一个的下标,所以计算的时候不影响
- 如果比前一个节点的高度高,就开始循环出栈,将那些比当前节点高度低的节点都出栈 出栈的时候也增加对应的蓄水量 其高度就是
Math.min(nowHeight, leftHeight) - midHeight,mid即当前有凹槽的节点的高度 宽度为nowIndex - left - 1,计算从当前节点往左,到前一个比凹槽点高度高的节点的长度 随后将对应的面积添加即可
public int trap(int[] height) {
int len = height.length;
int res = 0;
Deque<Integer> stack = new LinkedList<>();
stack.push(0);
for (int i = 1; i < len; i++){
Integer preHeight = height[stack.peek()];
if (height[i] < preHeight){
stack.push(i);
}else if (height[i] == preHeight){
stack.pop();
stack.push(i);
}else {
int nowHeight = height[i];
while (!stack.isEmpty() && nowHeight > height[stack.peek()]){
int mid = stack.pop();
if (!stack.isEmpty()){//说明mid的左边有比它高的柱子
int left = stack.peek();
int h = Math.min(nowHeight, height[left]) - height[mid];
int width = i - left - 1; // 凹槽的左边一个节点,如果存在才能蓄水
res += h * width;
}
}
stack.push(i);
}
}
return res;
}
以上的时间复杂度和空间复杂度都是O(n),加上双指针可以将其空间复杂度降低到O(1)