「青训营 X 码上掘金」 主题4 攒青豆

95 阅读3分钟

当青训营遇上码上掘金 本题来源于经典面试题:接雨水

方法一:双指针暴力

观察可知,每一列雨水的高度,取决于,该列 左侧最高的柱子和右侧最高的柱子中较矮的那个柱子的高度 - 当前柱子高度。

        int len = height.length;
        int res=0;
        for(int i=1;i<len-1;++i){
            int leftMax = Integer.MIN_VALUE;
            int rightMax = Integer.MIN_VALUE;    
            // 找左边的最高点        
            for(int j=0;j<i;++j){
                if(height[j]>leftMax){
                    leftMax=height[j];
                }
            }
            // 找右边的最高点
            for(int j=i+1;j<len;++j){
                if(height[j]>rightMax){
                    rightMax=height[j];
                }
            }
            int cur = Math.min(leftMax,rightMax)-height[i];
            if(cur>0){
                res+=cur;
            }
        }
        return res;

时间复杂度O(N^2)

方法二 动态规划

可以发现,在方法一中,每求一列的左右柱子最大高度都需要遍历整个数组,做了很多重复的计算。 实际上,【求取某个位置左边的最大高度】的这种问题,是可以划分成若干个子问题的,并且子问题相互独立

所以我们可以使用动态规划去解决这个问题。

我们只关心 i 位置左右的最大柱子高度,所以,为了避免重复计算,问题就转换成了,求两个目标数组leftMax[]rightMax[],分别存储左边最大高度和右边最大高度。 这样,此题就被我们转换成了一个非常经典的动态规划入门题目。

        int len = height.length;
        int res=0;
        int[] leftMax = new int[len];
        int[] rightMax = new int[len];

        // 动态规划解出每个位置左侧的最大高度
        for(int i=1;i<len;i++){
            leftMax[i]=Math.max(leftMax[i-1],height[i-1]);
        }

        for(int i=len-2;i>=0;i--){
            rightMax[i]=Math.max(rightMax[i+1],height[i+1]);
        }

        for(int i=1;i<len-1;i++){
            int cur = Math.min(leftMax[i],rightMax[i])-height[i];
            if(cur>0){
                res+=cur;
            }
        }
        return res;
        

时间复杂度O(N)

方法三 单调栈

上面的做法,都是以 为单位去计算。其实还可以以为单位去计算。其实,就是一个凹槽一个凹槽地计算。我们将凹槽看成矩形,大凹槽里还可以存在一个小凹槽,所以我们计算每个凹槽,相加即为最后结果。所有的凹槽,都是 高-低-高 的排列,

如果我们每次都单独得去判断 i 位置是不是某个凹槽的底部,并寻找凹槽的边界,我们就又回到了方法1中的那种暴力解法。

哪里可以优化呢?

我们可以利用单调栈的性质。我们保证递减的顺序入栈(即保证入栈的元素小于栈顶),当即将入栈的元素大于栈顶时,我们就找到了一个凹槽 ,栈顶就是凹槽的底部,即将入栈的元素和栈顶的下一个元素就是凹槽的两侧。

        // 保持递减入栈 栈内存放下标 按行计算
        int res = 0;
        ArrayDeque<Integer> ad = new ArrayDeque <>();

        for(int i=0;i<height.length;i++){
            // 入栈能保持递减
            if(ad.isEmpty() || height[ad.peekLast()] >height[i]){
                ad.addLast(i);
                continue;
            }
            if(height[ad.peekLast()]==height[i]){
                ad.pollLast();
                ad.addLast(i);
                continue;
            }
            // 当前i位置比栈顶还高,栈顶就是凹槽,所以栈顶位置的上方一定存在雨水
            while(!ad.isEmpty() && height[ad.peekLast()]<height[i]){
                // 栈顶就是最底部的了  当前要计算的雨水都在最底部的上面  计算完之后就不会再算入了
                // 所以要弹出
                int low = ad.pollLast();
                // 没有左边的柱子,接不了雨水
                if(!ad.isEmpty()){
                    // 左边的柱子 本次计算完后,左边的柱子还有可能成为底部,需要计算,所以不能弹出
                    int left = ad.peekLast();
                    // 宽度
                    int w = i - left-1;
                    // 高度
                    int h = Math.min(height[i],height[left])-height[low];
                    res+=w*h;
                }
            }
            ad.addLast(i);
        }
        return res;

注意,java的Stack性能很差,我们这里使用Deque作为栈。 时间复杂度O(N)