当青训营遇上码上掘金 - 攒青豆题目

81 阅读4分钟

当青训营遇上码上掘金,两者会碰撞什么样的火花呢?前几天成功进入了字节跳动青训营,发现青训营里青豆可以说是十分重要,为了结营证书大家都在努力,本次活动的攒青豆题目也是十分应景。接下来就让我们看一下攒青豆题目的具体描述吧。

题目描述

现有 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个单位的青豆,所以输出17。

解题分析

好了,相信大家现在已经了解题目了,那么我们应该如何去解题呢?

首先我们看到输入是一个数组,有的同学就会考虑,是否可以定义两个指针,找到两个柱子之间所攒的青豆呢?所以会考虑使用双指针的方式。双指针的方式是可以解题的,除此之外我们还可以利用单调栈这个数据结构来进行解题。

下面就来看一下这两种方式的实现思路吧。

双指针法

如果你一开始就考虑到使用双指针法,那么你肯定想的是按照列来求解青豆的数量(即求出每一列青豆的高度)。那么恭喜你,你的第一步思路是正确的,那么应该如何求解每一列青豆的高度呢?

实际上,每一列青豆的高度取决于其左侧最高柱子和右侧最高柱子中较低的那一个。我们通过下面的图片来理解。 image.png 图中是求解第一列青豆的高度,结果是4,是不是很容易理解,这样我们只需要遍历一遍数组,求出所有青豆的高度,然后加到结果中去,就可以了。

但是我们要注意的是,第一个列左边不会有柱子,最后一个列右边也不会有柱子,所以第一列和最后一列肯定不会收集到青豆,就不用参与计算了。

看一下代码实现:

import java.util.*;
public class Main {
	public static void main(String []args) {
		// Scanner in = new Scanner(System.in);
		// int n = in.nextInt();
		// int height = new int[n];
		// int i = 0;
		// while (in.hasNextInt() && i < n) {
		// 	height[i] = in.nextInt();
		// 	i++;
		// }
		int[] height = {5,0,2,1,4,0,1,0,3};
		int res = trap(height);
		System.out.println(res);
	}
        public static int trap(int[] height) {
        int sum = 0;
        for (int i = 0; i < height.length; i++) {
            if (i==0 || i== height.length - 1) continue;
            int rHeight = height[i];
            int lHeight = height[i];
            for (int r = i+1; r < height.length; r++) {
                if (height[r] > rHeight) rHeight = height[r];
            }
            for (int l = i-1; l >= 0; l--) {
                if(height[l] > lHeight) lHeight = height[l];
            }
            int h = Math.min(lHeight, rHeight) - height[i];
            if (h > 0) sum += h;
        }
        return sum;
    }
}

单调栈

接下来我们看一下单调栈应该如何去实现呢?

其实,如果要使用单调栈的话,我们就要转换一下思路,不应该按照每一列来求青豆的数量,而是按照每一行来求青豆的数量,我们看下面的图片来理解一下。

image.png 那么为什么要这么做呢?接下来我们看一下使用单调栈求解的过程,我相信大家就能够理解了。

在本题中,我们需要将较高的柱子(即大元素)放在单调栈栈底,将较低的柱子放在单调栈栈头,即单调栈从栈底到栈头应该是从大到小的顺序。因为我们发现比当前柱子小的元素,是攒不到青豆的,如果发现了比栈头柱子大的元素,就会有一个凹槽攒到青豆,这时候需要记录结果。如何更新结果呢?

这时候我们将栈顶元素弹出,弹出的元素就是凹槽底部,此时新的栈顶元素为凹槽左边界,遍历到的元素为凹槽右边界。结果值需要加上凹槽左右边界两个柱子的距离 * 青豆的高度(即凹槽左右柱子较低的一个 - 凹槽底部高度)。这下大家理解为什么要按照每一行来求解青豆的数量了吧。

如果遇到高度相同的柱子,我们只需要把原来的柱子出栈,用新的柱子来代替就好了。

注意,栈中我们只需要保存数组的下标即可。

代码实现如下:

import java.util.*;

public class Main {
	public static void main(String []args) {
		// Scanner in = new Scanner(System.in);
		// int n = in.nextInt();
		// int height = new int[n];
		// int i = 0;
		// while (in.hasNextInt() && i < n) {
		// 	height[i] = in.nextInt();
		// 	i++;
		// }
		int[] height = {5,0,2,1,4,0,1,0,3};
		int res = trap(height);
		System.out.println(res);
	}
    public static int trap(int[] height) {
        int res = 0;
        Deque<Integer> stack = new LinkedList<>();
        stack.push(0);
        for (int i = 1; i < height.length; i++) {
            if (height[i] < height[stack.peek()]) {
                stack.push(i);
            } else if (height[i] == height[stack.peek()]) {
                stack.pop();
                stack.push(i);
            } else {
                while (!stack.isEmpty() && height[i] > height[stack.peek()]) {
                    int mid = stack.pop();
                    if (!stack.isEmpty()) {
                        int left = stack.peek();
                        int h = Math.min(height[left], height[i]) - height[mid];
                        int w = i - left - 1;
                        res += (h * w);

                    }
                }
                stack.push(i);
            }
        }
        return res;
    }
}

最后希望大家都能够多攒青豆,拿到结营证书,一起努力吧!