当青训营遇上码上掘金 | 攒青豆题解

28 阅读3分钟

当青训营遇上码上掘金

题目:

  • 攒青豆

    现有 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 个单位的青豆。、

👨‍🏫 题解传送门

👨‍🏫 核心思想

咋一看,整体求解有点难,那就可以分开来求,分析每一根柱子上面可以攒多少青豆,类似于微元法 根据短板效应 得某一位置上能攒的青豆数 主要受 该位置两边的最高柱子 中的较矮的 柱子高度 限制

👨‍🏫 暴力求解的主要思路 O(n^2):

  1. 求该位置左边最高的柱子的高度 lMax
  2. 求该位置右边最高的柱子的高度 rMax
  3. min(lmax,Rmax) - 该位置的height = 青豆数

注意:当该位置的柱子是左或者右最高柱子的时候,攒不了青豆 并且左右边界柱子也是攒不了青豆的,可以直接跳过

👨‍🏫 记忆表优化 O(n)

对于暴力求解的方法进行分析,就会发现在求左右最高柱子的时候存在大量的重复计算,这部分是最容易想到可以优化的地方,以空间换时间的办法之一,就是开一个记忆表存放某位置的左右最高柱子高度,建立层层依赖的关系 例: 左边最高柱子的高度 第一个是本身,第二个就是本身与第一个的高度比较大小,第三个就是本身与第二个比较大小即可...... 这样下来求最高柱子便可以省掉重复的操作

主要思路:

  1. 循环填左右最高柱子的记忆表
  2. 根据记忆表求出每个位置的青豆数(特判:当最高柱子 == 本身高度 时,攒的青豆数为 0,可以直接跳过)

👨‍🏫 拓展

🐷 单调栈:元素具有单调性的栈

🐷 单调栈求解 O(n):

上边两个方法都是 分开求每一个横轴单位上(列)的青豆数 而单调栈是 按 层 来求某一个凹槽上可以攒的青豆 凹槽特性,柱子高度先单调递减,再单调递增

主要思路:

  1. 维护一个单调递减的栈,栈存柱子的下边,当前柱子的高度比栈顶柱子的高度 小,入栈

  2. 否则,即是凹槽,栈顶元素出栈(注意相同的元素)

  3. 此凹槽的青豆数 = (min(栈顶柱子的高度,当前柱子的高度)- 出栈柱子的高度)* (当前柱子的下标 - 栈顶柱子的下标 - 1)

    // ...下标-1 : 例: 右柱 3,左柱 1, 3-1 = 2,但实际只有中间的 2柱 上能攒豆,so: 3-1-1 = 1

  4. 当前柱子进栈

  5. 循环往复,直到栈空,返回结果

/**
	 * 单调栈求解法
	 * 
	 * @param height 高度数组
	 * @return 攒的青豆数
	 */
	public static int getBeans3(int[] height)
	{
//		思路:横向青豆,就是求 凹槽 先递增后递减
		int res = 0;
		Deque<Integer> stack = new ArrayDeque<>();
		for (int i = 0; i < height.length; i++)
		{
			while (!stack.isEmpty() && height[i] > height[stack.peek()])
			{
				int top = stack.pop();
				while (!stack.isEmpty() && height[top] == height[stack.peek()])
				{
					stack.pop();// 需要考虑重复元素
				}
				if (!stack.isEmpty())
				{
					int min = Math.min(height[i], height[stack.peek()]);// 取当前遍历值 与 栈顶元素的较小值
					res += (min - height[top]) * (i - stack.peek() - 1);// -1 是因为在中间才能攒豆
				
				}

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