42. 接雨水

目录:

题目

给定 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 * 10^4
  • 0 <= height[i] <= 10^5

第一种方法:动态规划(使用额外的数组存储临时数据)

原理一个位置处能存储的水的多少与这个位置左、右边的最大值中的最小值有关(木桶原理)如果这个最小值大于当前的高度,则当前位置能存储的雨水为这个高度差

基于动态规划Dynamic Programming的,算法步骤:

  • 我们维护两个个一维的dp数组,这个DP算法需要遍历两遍数组,
    • 第一遍遍历,存入i位置左边的最大值到leftMax中,
    • 第二遍遍历数组,存入i位置右边的最大值到rightMax中,
  • 然后找出左右两边最大值中的较小值,然后跟当前值height[i]相比,如果大于当前值,则将差值叠加到结果
class Solution {
    public int trap(int[] height) {
        int n = height.length;
        if(n < 3)
        {
            return 0;
        }
        int[] leftMax = new int[n];
        int[] rightMax = new int[n];
        for(int i = 1;i<n;i++)
        {
            leftMax[i] = Math.max(leftMax[i-1],height[i-1]);
        }
        for(int i = n-2;i>=0;i--)
        {
            rightMax[i] = Math.max(rightMax[i+1],height[i+1]);
        }
        int ret = 0;
        for(int i = 1;i<n-1;i++)
        {
            int cur = Math.min(leftMax[i],rightMax[i])-height[i];
            if(cur>0)
            {
            ret += cur;
            }
        }
        return ret;

    }
}

第二种方法:改进的动态规划(使用变量存储临时中间数据) 

基于上面每一个位置能否存储雨水的原理,我们不必额外的开辟数组存储两边的最大值信息,我们可以先找出数组中的最大值,这样一个位置一侧的最大值就确定(固定)住了,另一侧的最大值,根据上面动态规划中的递推公式,只需要一个变量进行记录更新就行了

class Solution {
    public int trap(int[] height) {
        int n = height.length;
        if(n < 3)
        {
            return 0;
        }
        int theMax = height[0];
        int maxIndex = 0;
        for(int i = 1;i<n;i++){
            if(height[i]>height[maxIndex]){
                theMax = height[i];
                maxIndex = i;
            }
        }
        int ret = 0;
        int leftMax = 0;
        for(int i = 1;i<maxIndex;i++){
            leftMax = Math.max(leftMax,height[i-1]);
            int temp = Math.min(leftMax,theMax)-height[i];
            if(temp>0){
                ret+=temp;
            }

        }
        int rightMax = 0;
        for(int i = n-2;i>maxIndex;i--){
            rightMax = Math.max(rightMax,height[i+1]);
            int temp = Math.min(rightMax,theMax)-height[i];
            if(temp>0){
                ret+=temp;
            }
            
        }
        return ret;

    }
}

第三种方法:双指针排除移动扫描法

对于位置index,

  • 如果能确定这个位置处的储水情况,就可以将指针越过这个位置
    • 不能储水,直接跳过
    • 能储水,计算此位置处的储水量后,移动跳过
  • 对于可能存在的潜在的能储水的左右两个位置,现在还不能肯定的下结论,就只能保持不动

总之,我们是使用两个指针从两边向中间扫,

  • 一定不会发生遗漏的需要判断的位置
  • 能肯定的下结论的,移动这个指针
  • 目前情况下,不能下结论的,指针保持不动
  • 彼此间相互判断

实现1

class Solution {
    public int trap(int[] height) {
        int n = height.length;
        if(n < 3)
        {
            return 0;
        }
        int ret = 0;
        int leftMax = 0,rightMax = 0;
        int i = 0,j = n-1;
        while(i<=j)
        {

            //此时i位置处不可能能储水,故i位置处可以排除,移动跳过
            while(i<=j && height[i]>=leftMax)
            {
                leftMax = height[i];
                i++;
            }
            if(i>j)
                return ret;
            //此时j位置处不可能能储水,故j位置处可以排除,移动跳过    
            while(j>=i && height[j]>=rightMax)
            {
                rightMax = height[j];
                j--;
            }
            if(j<i)
                return ret;
            //此时i,j处如果能储水单侧所需要满足的条件都已经具备,即: 
            //leftMax>height[i],height[j]<rightMax
        	else  
        	{   
                //height[i] < leftMax < rightMax
                //此时能够确定i处左右两侧的最大值中的最小者
        		if(leftMax<rightMax)
        		{
        			ret+=leftMax-height[i];
        			i++;
        		}
        		else
        		{   
                    //leftMax >= rightMax > height[j]
                    //此时能够确定j处左右两侧最大值中的最小者
        			ret+=rightMax-height[j];
        			j--;
        		}
            }
        }

        return ret;
        

    }
} 

实现2

class Solution {
    public int trap(int[] height) {
        int n = height.length;
        if(n<3)return 0;
        int ret = 0;
        //上一次的leftMax,rightMax初始化值
        int leftMax = 0,rightMax = 0;
        //本轮即将判断的位置对
        int i = 0,j = n-1;
        while(i<=j)
        {
            //先更新一下本轮需判断的位置对i,j的leftMax,rightMax值
            //之所以在这个位置更新,是为了防止index超越数组下标范围
            //leftMax含义:数组[0,i]范围内的最大值
            //rightMax含义:数组[j,n-1]范围内的最大值
            leftMax = Math.max(leftMax,height[i]);
            rightMax = Math.max(rightMax,height[j]);
            if(leftMax<=rightMax)
            {   
                //如果此时i处的值大于它左边的最大值,则i处不能储水
                //而根据leftMax值的更新公式,此时的leftMax等于height[i]
                //于是i处的储水值等于自己减自己,等于0,虽然i处不能储水,但是
                //将它的储水值设定为0增加到ret上,不影响最后的结果
                ret+=leftMax-height[i];
                i++;
            }
            else
            {
                ret+=rightMax-height[j];
                j--;
            }
        }

        return ret;

    }
} 

第四种方法:单调栈

  • 栈中保存的元素非递增,作为潜在的卡槽的左挡板,由于栈中元素非递增,相当于卡槽能储水左边需满足的条件自然满足
  • 从左到右遍历数组,如果当前元素大于栈顶元素,则当前元素能够作为卡槽的右挡板
  • 这个算法是一层一层地计算存储的雨水量的
  • 用例子模拟整个过程就可以写出完整的代码(不要技巧化,根据例子直接模拟写代码)
  • 栈里保存的是下标,这样我们不丢失由于栈中元素的弹出而导致损失的位置信息 
class Solution {
    public int trap(int[] height) {

        int n = height.length;
    	if(n <3)
    		return 0;
        int ret = 0;
        Deque<Integer> s = new LinkedList<>();
        int i = 0;
        while(i < n) 
        {   
            if(s.isEmpty() || height[i] <= height[s.peek()])
            {
            	s.push(i);
            	i++;
            }
            else 
            {
                int index = s.peek(); //作为卡槽的底部
                s.pop();
                if(s.isEmpty()) //卡槽能储水,至少需要三个元素,左挡板,底部,右挡板
                {
                	s.push(i);
                	i++;
                }
                else 
                	ret += (Math.min(height[i], height[s.peek()]) - height[index])*(i-s.peek()-1);
            }
        }
        return ret;
    }
}

posted on 2022-06-02 15:23  朴素贝叶斯  阅读(54)  评论(0编辑  收藏  举报

导航