当青训营遇上码上掘金——攒青豆

67 阅读5分钟

当青训营遇上码上掘金

攒青豆

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

image.png
以下为上图例子的解析:
输入:height = [5,0,2,1,4,0,1,0,3]
输出:17
解析:上面是由数组 [5,0,2,1,4,0,1,0,3] 表示的柱子高度,在这种情况下,可以接 17 个单位的青豆。

解法一 暴力解法

思路

通过观察规律,可以知道,每个柱子所能接到的青豆数取决于两个重要的因素,第一个是该柱子左侧高度最高的柱子,另外一个是该柱子右侧高度最高的柱子。两柱子之间高度较小的即为该柱子在接完青豆后所能达到的最大值。 因此按照该思路,我们可以写出以下代码

代码

public int getBeans(int[] arr){
    int res = 0;
    for (int i=0;i<arr.length;i++){
        int leftMax = 0;
        int rightMax = 0;
        for (int j = i; j >= 0; j--) {
            leftMax = Math.max(leftMax, arr[j]);
        }
        for (int j = i; j < arr.length; j++) {
            rightMax = Math.max(rightMax, arr[j]);
        }
        res+=Math.min(leftMax,rightMax)-arr[i];
    }
    return res;
}

算法分析

该代码首先遍历该数组,对于数组中的每一个元素,分别利用两个循环来寻找其左侧和右侧的最高柱子高度,记为leftMaxrightMax,然后取两者之中的最小值。按照之前的思路,

柱子高度+青豆数=左右侧柱子中的最小值(包括自己)柱子高度+青豆数=左右侧柱子中的最小值(包括自己)

,因此可以得到

青豆数=左右侧柱子中的最小值(包括自己)柱子高度青豆数=左右侧柱子中的最小值(包括自己)-柱子高度

,所以该柱子所能接到的青豆数如上式所示,然后将每个柱子所能接到的青豆数加到res中,就可以得到所能接到的所有青豆数。

以第3列为例,该柱子高度为2,其左侧柱子最高为5,右侧柱子最高为4,因此其所能接到的青豆数为

42=24-2=2

,图解如下图所示

image.png

复杂度分析

对于时间复杂度来说,采用两层循环,每一层循环都遍历了整个数组,因此时间复杂度为O(N2)O(N^2)

对于空间复杂度来说,只用到了两个临时变量,因此空间复杂度为O(1)O(1)

解法二 双指针解法

思路

由解法一可注意到,在遍历过程中有很多不必要的计算。我们在求解过程中,每次都需要找到的是该柱子左右两侧最高柱子中的最小柱子,而在从前往后遍历的过程中,我们可以很容易的知道左侧的最高柱子的高度(只需要一个变量来记录),因此,此时需要与右侧最高柱子的高度进行比较。

此时想一想,真的要和右侧最高柱子高度比较吗?

如果右侧有一个比左侧最高柱子更高的(但不一定是最高的),那么还有必要继续搜索下去吗?答案很明显,是不用的,原因是我们只需要两者之间更小的,而此时必然左侧的更小,因此直接可以得到该柱子所能接到的青豆数。

那么再想想,右侧柱子的遍历只能从该柱子到结尾吗?能不能从后往前遍历呢?

答案也很明显,是可以的。那么此时可以发现,左右两侧是不是几乎完全等效的?一个从前往后,一个从后往前,区别只有前后之差。之前提到,只需要找到比左侧柱子更高的右侧柱子,就可以确定当前柱子的青豆数,那么假设我们从后往前遍历,是不是只需要找到一个比右侧最高(从后往前遍历,同样可以使用一个遍历记录最高柱子)更高的左侧柱子(无需最高)?那么答案就出来了。我们只需要使用两个指针,一个从前往后遍历(即为左指针),一个从后往前遍历(即为右指针),这样,从前往后遍历记录了左指针指向的柱子的左侧最高柱子的值,从后往前遍历记录了右指针指向的柱子的右侧最高柱子的值。对于两指针来说,右指针右侧的柱子都是左指针右侧的柱子,因此只要左指针左侧最高柱子比右指针右侧最高柱子更低,就可以直接得到左指针指向柱子所能接到的青豆,右指针同理。左右指针交叉遍历,在相遇之前就可得到结果。

代码

public int getBeans(int[] arr){
    int res = 0;
    int left = 0, right = arr.length - 1;
    int leftMax = 0, rightMax = 0;
    while (left <= right) {
        leftMax = Math.max(leftMax, arr[left]);
        rightMax = Math.max(rightMax, arr[right]);

        if (leftMax > rightMax) {
            res += rightMax - arr[right];
            right--;
        } else {
            res += leftMax - arr[left];
            left++;
        }
    }
    return res;
}

算法分析

该代码使用双指针分别从数组的头部和尾部分别遍历,在遍历过程中,leftMax记录左指针左侧最高柱子,rightMax记录右指针右侧最高柱子。当左指针左侧最高柱子更高时,说明右指针指向的柱子的右侧最高柱子更低,也就能得到右指针所指向柱子所能接到的青豆数,左指针同理。

复杂度分析

对于时间复杂度来说,左右指针在相遇之后就停止了遍历,因此时间复杂度为O(N)O(N)

对于空间复杂度来说,只有到了几个指针变量和临时变量,因此空间复杂度为O(1)O(1)

代码片段

查看代码请点击下方Script

总结

本文记录了对攒青豆问题的解决方法,主要提出了两种思路,暴力解法虽然低效,但是易于理解;双指针方法高效,但要想到需要费点功夫。

文章到这就结束了,如有错误,还请指正~