关于攒青豆

109 阅读3分钟

当青训营遇上码上掘金。

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

输入:height = [5,0,2,1,4,0,1,0,3]

输出:17

解析:上面是由数组 [5,0,2,1,4,0,1,0,3] 表示的柱子高度,在这种情况下,可以接 17 个单位的青豆。

这道题乍看很难,不知从何下手,但我们可以结合生活实际,分析以下攒青豆的规则,简单的说就是两边兜着,中间取个水平面,水平面之下除了柱子的面积就是青豆的面积。就有点像接雨水的情形,这道题我曾经在力扣做过,想了好久都没有解法,看了评论区的大佬,学习了他的思路,最后ac了。

我就从三个思路分析

1.按行数

从底下往上一行一行数有多少青豆,存住青豆的要求就是左边有柱子,右边有柱子中间就都是豆子。 具体实现就是当某一列出现高于这行高度的柱子,就把它作为左边界,然后向右数,加到int temp里,如果遇到了又一列高于这行高度的柱子,就作为右边界,temp加到sum里,然后清零,如此遍历一整行 代码如下: public int trap(int[] height) { // 1.按行数 int sum = 0; int heighest = 0; for (int i = 0; i < height.length; i++) { if (height[i] > heighest) { heighest = height[i]; } } int row_sum; boolean flag; int temp; for (int h = 1; h <= heighest; h++) { // 从第一行开始数共有多少豆 row_sum = 0; flag = false; temp = 0; for (int i = 0; i < height.length; i++) { // 计算每行的豆 if (!flag && height[i] >= h) flag = true; // 开始计数 if (flag) { if (height[i] < h) { temp++; } else { row_sum += temp; temp = 0; } } } sum += row_sum; } return sum; } 这个方法确实容易想到,但是存在较大问题,那就是把本来一维的问题变成了二维处理,时间复杂度高。 时间复杂度:如果最大的数是m,个数是n,那么就是 O(m*n)。

空间复杂度:O(1)。

2.按列数

这种解法思路:某一列的高度取决于该列两边最高边的较矮者,那么就很容易想到需要求出每一列的两边最高边是多少。这就需要动态规划的思路,提出一个数组max_left[i],表示索引为i的那列的左边所有柱子中最高值。很容易想到迭代关系:max_left[i] = max(max_left[i-1],height[i])以此遍历就可以得到某列左边最高边的数组,同理从右向左迭代可以得到某列右边最高边的数组max_right[i]。

然后从左到右遍历每一列,每一列的豆子面积等于该列两边最高边的较低者与此列的差值,如果此列高于较低者,那么这列没有豆子。 代码如下: public int trap2(int[] height) { int sum = 0; // 按列数+动态规划 // 空间换时间 int[] max_left = new int[height.length]; // max_left[i]表示索引为i的列其左边最高值 max_left[0] = 0; // 左边没有墙,先定为0 int[] max_right = new int[height.length]; // max_right[i]表示索引为i的列右边最高值 max_right[height.length - 1] = 0; // 某一列所含的水含量为其两边的最高列中较低者与自己的差值;若较低者比自己低,则此列没有水。 // 先求最高列数组 for (int i = 1; i < height.length; i++) { max_left[i] = Math.max(max_left[i - 1], height[i - 1]); } for (int i = height.length - 2; i >= 0; i--) { max_right[i] = Math.max(max_right[i + 1], height[i + 1]); } int lowerWall; for (int i = 0; i < height.length; i++) { // 求各列的水柱长度 lowerWall = Math.min(max_left[i], max_right[i]); if (lowerWall > height[i]) { sum += (lowerWall - height[i]); } } return sum; } 这么做空间消耗大了些,但是时间快了不少 长度是n,空间复杂度:O(n),时间复杂度:O(n)。

3.双指针从两边往中间按列数

因为max_left、max_right数组中的数据都只用一次,所以很容易想到可以边求面积,边更新max_left,而不是作为数组全部记录。我想到如果最右边就是全图最高的列,那么min(max_left,max_right)就一直是max_left,每次都只要更新左边最高值,然后这列的面积就是max_left和这列的差值(这列高就为0)。

并不总是如意,如果最高边在在左边,那我们就可以从右边往左边数,所以我们就提出了双指针法,保持当前最高边不动,另一边移动。这样就可以只顾一边,更新该列外边(左指针更新max_left,右指针更新max_right)的最高值代码如下 public int trap3_1(int[] height) { int sum = 0; // 思路分析:动态规划得到的数组中的数据其实都只用了一次,用的时候更新到即可 // 可以随按列数的坐标变动。但是max_left 确实可以随着变动,但max_right不行。 // 所以提出双指针法,有水的只有中间n-2列left从1往右,right从n-2往左 // 每次移动规则是移动矮边,保持长边不动 int left = 1; int right = height.length - 2; int max_left = 0; int max_right = 0; while (left <= right) { if (height[left-1] > height[right+1]) { max_right = Math.max(max_right,height[right + 1]); // 左边高,右边数一列 if (max_right > height[right]) sum += max_right - height[right]; right --; } else { // 右边高,左边数一列 max_left = Math.max(max_left,height[left - 1]); if (max_left > height[left]) sum += max_left - height[left]; left ++; } } return sum; } 这个方法克服了上面两个算法的缺点,既快又省空间 height数组长度n,空间复杂度O(1),时间复杂度O(n)