当青训营遇上码上掘金-(主题四)攒青豆

52 阅读2分钟

当青训营遇上码上掘金。 这题类似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) - midHeightmid即当前有凹槽的节点的高度 宽度为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)