当青训营遇上码上掘金

14 阅读4分钟
  • 主题 4:攒青豆

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

攒青豆.png

根据题意,我们可以将数组中的每个元素看成一个柱子,两个柱子间就可以组成一个盛青豆的容器。我们可以通过左右两个双指针来表示这个容器,初始化的容器则由最左边的柱子和最右边的柱子组成(最宽),然后再根据数组中的元素去细化容器的值。我们通过以下两种情况来具体分析。

第一种情况:左右柱子即为区间内最高的柱子

以 height[4,2,0,3,2,5]为例, 如图所示。

image.png

这里可以通过两个指针来表示我们的容器,初始化的容器则由最左边的柱子和最右边的柱子组成(最宽),如下图所示,由最左边的height[0] = 4 和最右边的height[5] = 5 组成 。此时我们假想中间没有任何柱子,则此时的容器能接的青豆为container = (r - l- 1) * min( height[l], height[r] ),即16。

image.png

但实际情况中间一般是有柱子的,所以我们的指针以柱子较低的一方开始移动(较低的一方决定了能能接青豆的上限),此时l指针指向坐标1,也就是height[l] = 2。此时我们发现该柱子是比左边最高的柱子更低,所以当指针移动到1的时候,我们可以很轻松的计算当前的container = container - height[l] = 16 - 2 = 14。

image.png

当前的柱子比左边最高的柱子低,当然也就比右边最高的柱子低了,所以我们可以继续遍历,发现数组中的柱子都比初始的柱子低,则可以直接计算得出container = container - 0 - 3 - 2 = 9。

image.png

这里需要注意的是,容器能接青豆的多少取决于较低的那一方,所以我们的指针总是从较低的那一方开始移动。如果初始化右边更低,则先移动r指针。

现在我们可以写出初版的代码

public class Solution {

/**
 * 双指针
 * @param height
 * @return
 */
public int trap(int[] height) {
    if(height.length == 1) return 0;
    int l = 0, r = height.length-1;
    int container = (r - l - 1) * Math.min(leftMax, rightMax);

    while (l < r) {
        if (height[l] <= height[r]) {
            l++;
            if (l >= r) break;
            if (height[l] < leftMax) {
                container -= height[l];
            }
        } else {
            r--;
            if (l >= r) break;
            if (height[r] < rightMax) {
                container -= height[r];
            }
        }
    }
    return container;
}

}

第二种情况:左右柱子之间有更高的柱子

如果我们将数组改为[4,2,0,6,2,5],入下图所示。

image.png

此时,我们仍然按之前的思路,以左右两边的柱子作为初始化容器container = (r - l- 1) * min( height[l], height[r] ) = 16,然后从较低的那一侧的柱子开始移动指针。

image.png

直到我们的l指针移动到3,发现height[3] = 6,这个时候我们就不能直接container-height[l]了,因为我们的容器被这个更高的柱子分割成了两个部分,如下图。

image.png

出现这种情况一定是当前的柱子比左边最高的那个柱子更高,这个时候我们就需要标记一下左边最高的那个柱子的高度,用leftMax表示。同理我们也需要用rightMax标记右边最高的那个柱子。

其实到这个时候,我们就可以抛弃掉当前柱子左半边的部分了,以当前柱子作为一个新的开始,继续计算。因为左边部分能够盛豆的部分我们已经计算过了。那么左边实际上我们算出来的盛的青豆是多少呢?看下面这个图就明白了。

image.png

image.png

所以,左边实际能够盛青豆的值=container-未计算的部分,即leftCalculated = container - leftMax * (r - l);

OK,现在左半部分我们就已经跟他撇清关系了,现在我们可以将关注点看向右边了。这个时候我们将当前柱子看为leftMax,原来的rightMax不变,重新计算右半部分的假想最大rightContainer,即rightContainer = (r - l - 1)* Math.min(height[l], height[r])。

由于我们是将container这个值作为最终结果返回的,所以当前的container为左边已经计算的实际能盛青豆的值 + 右边假想的最大container的值 container = leftCalculated + rightContainer;

重置完container的值后,我们就可以继续右边部分的遍历了,按之前的逻辑即可。