「青训营 X 码上掘金」攒青豆(接雨水变形)

181 阅读4分钟

当青训营遇上码上掘金

题目介绍

现有 n 个宽度为 1 的柱子,给出 n 个非负整数依次表示柱子的高度,排列后如下图所示,此时均匀从上空向下撒青豆,计算按此排列的柱子能接住多少青豆。(不考虑边角堆积)
image.png

题目解法

1.双指针

知识点

双指针指的是在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个指针(特殊情况甚至可以多个),两个指针或是同方向访问两个链表、或是同方向访问一个链表(快慢指针)、或是相反方向扫描(对撞指针),从而达到我们需要的目的。

思路

我们都知道水桶的短板问题,控制水桶水量的是最短的一条板子。这道题也是类似,我们可以将整个图看成一个水桶,两边就是水桶的板,中间比较低的部分就是水桶的底,由较短的边控制水桶的最高水量。但是水桶中可能出现更高的边,比如上图第四列,它比水桶边还要高,那这种情况下它是不是将一个水桶分割成了两个水桶,而中间的那条边就是两个水桶的边。

有了这个思想,解决这道题就容易了,因为我们这里的"水桶"有两个边,因此可以考虑使用对撞双指针往中间靠。

具体做法

  • step 1:检查数组是否为空的特殊情况
  • step 2:准备双指针,分别指向数组首尾元素,代表最初的两个边界
  • step 3:指针往中间遍历,遇到更低柱子就是底,用较短的边界减去底就是这一列的接水量,遇到更高的柱子就是新的边界,更新边界大小。

image.png

代码实现

public class Solution {
    public long maxWater (int[] arr) {
        //排除空数组
        if(arr.length == 0) 
            return 0;
        long res = 0;
        //左右双指针
        int left = 0; 
        int right = arr.length - 1; 
        //中间区域的边界高度
        int maxL = 0; 
        int maxR = 0;
        //直到左右指针相遇
        while(left < right){ 
            //每次维护往中间的最大边界
            maxL = Math.max(maxL, arr[left]); 
            maxR = Math.max(maxR, arr[right]);
            //较短的边界确定该格子的青豆数
            if(maxR > maxL) 
                res += maxL - arr[left++]; 
            else
                res += maxR - arr[right--];
        }
        return res;
    }
}

解法二

思路

求出每个柱子上面能够存多少青豆,然后将每根柱子的存豆量相加便能得到总的存豆量,为求出每根柱子上能够存多少青豆,就要求出每根柱子左边最高的和右边最高柱子,然后用两者的最小值减去当前柱子的高度。 例如图中从左到右第二根柱子的高度为2,它左边最高柱子的值为5,右边最高柱子的值为4,因此它的最大存豆量为 Min(4,5)-2=2。

image.png
利用上面思路,从左到右遍历每根柱子,遍历的时候求出每根柱子左边最高和右边最高柱子的值,然后利用s两者的最小值减去当前柱子的高度就行了。

代码实现

    {
        int bean=0;
        //bean 用于保存最大存豆量
        if(arr==null||arr.length<=1)
            return 0;
            //如果只有一根柱子围不住青豆。
        int leftLargest=0,rightLargest=0;
        //leftLargest,rightLargest 分别用于保存遍过程中,当前元素左边最大值,和右边最大值。
        for(int i=0;i<arr.length;i++)
        {   
            leftLargest=0;
            rightLargest=0;
            for(int j=0;j<i;j++)
                leftLargest=Math.max(leftLargest,arr[j]);
                //先求出当前元素左边最大值。         
            for(int j=arr.length-1;j>i;j--)
                rightLargest=Math.max(rightLargest, arr[j]);
                //最求出当前元素右边最大值          
            water+=Math.min(leftLargest, rightLargest)>arr[i]?Math.min(leftLargest, rightLargest)-arr[i]:0;
//左边最大值和右边最大值的最小值与当前元素比较如果小于当前元素,则当前元素围不住青豆,如果大于当前元素,则减去当前元素得到存豆量。
        }                               
        return bean;
    }

注意:如果当前柱子大于它左右最大值的任何一个是存不了水的

解法三

思路

上个解法对于每个元素都要从左到右,和从右到最左遍历其两边最大值,假如使用两个数组 left[ ] , right[ ]来保存每个元素左边最大值,右边最大值的话,这样就不用每次都遍历了。如下图:对于数组{ 5, 2 , 6 , 2 , 4 }它的左右数组如下:
左数组:

image.png
右数组:

image.png

代码实现

    {
        int bean=0;
        if(arr==null||arr.length<=1)
            return 0;
        int leftLargest=0,rightLargest=0;
        int left[]=new int[arr.length];
        //left 数组中保存每个元素左边的最大值,left[i],表示数组中第i个元素的左边最大值。
        int right[]=new int[arr.length];
        //right数组中保存每个元素左边的最大值,right[i],表示数组中第i个元素的右边最大值。
        for(int i=0;i<arr.length;i++)
        {
            leftLargest=Math.max(leftLargest,arr[i]);
            left[i]=leftLargest;            
        }
        //先遍历一次找出每个元素左边最大值。
        for(int i=arr.length-1;i>=0;i--)
        {
            rightLargest=Math.max(rightLargest,arr[i]);
            right[i]=rightLargest;          
        }
        //遍历找到每个元素右边最大值。
        for(int i=0;i<arr.length;i++)
        {           
            bean+=Math.min(left[i],right[i])>arr[i]?Math.min(left[i],right[i])-arr[i]:0;           
        }   
        //利用当前元素的左边最大值和右边最大值求得存水量。  
        return bean;
    }

上面算法的流程:
①从左到右遍历一次求出每个元素左边的最大值,保存在 left 数组中。
②从右到左遍历一次求出每个元素右边的最大值,保存在right数组中。
③最后一次遍历求出每个元素(每根柱子)的存豆量。

解法四

思路

从最底层开始计算水量,只要两端的方柱高端不为0,那么最底层的水量=高度为零的方柱数。

计算完毕后将所有不为0的方柱高度下降1格,得到次底层的情况,继续由上述规则计算。

循环直至不为零的方柱数<2

代码实现

                int left=0;
		int right=n-1;
	
		int count = 0;//计数
		while(left<right){
			//判断两边高度是否为0
			if(height[left]==0){
				left++;
				continue;
			}
			if(height[right]==0){
				right--;
				continue;
			}
			//判断柱子为0的数量,即为此层青豆的数量
			for(int k=left+1;k<right;k++){
				if(height[k]==0){
					count++;
				}
			}
			//将最底层抹平,次底层降为底层
			for(int k=left;k<=right;k++){
				if(height[k]>0){
					height[k]-=1;
				}
			}
		}
		return count;


  }

本人才疏学浅,如有疏漏还望见谅!