青训营 X 码上掘金-主题 4:攒青豆

95 阅读3分钟

当青训营遇上码上掘金

主题 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 个单位的青豆。

暴力求解

当我们拿到一道题,没有很好的思路时,我们首先总是想到能不能用暴力的手段解决问题,当然这道题也是可以的.

那么这道题如何暴力求解呢,我们需要遍历数组的每一个位置。对于每一个位置,我们需要枚举出来它左边的最大高度和它右边的最大高度,然后取两边高度小的那个在减去当前柱子的高度,这就是这个位置能装的青豆的数量,在遍历的过程中将每一个位置能存放的青豆数量累积起来,就可以得到我们要的结果.

总结来说就是每个位置的柱子小于两边的高度时,能装的青豆数为:

res += min(leftMax,rightMax)-height[i]

代码实现:

func trap(height []int) int {
   // 小于三个柱子,肯定不会有水,直接返回
   if len(height) <= 2 {
      return 0
   }
   res := 0
   // 第一根柱子和最后一根柱子一定不会有青豆,所以从第二根柱子开始遍历
   for i := 1; i < len(height)-1; i++ {
      // 求出左边的最大值
      leftMax := 0
      for j := 0; j < i; j++ {
         leftMax = max(leftMax, height[j])
      }
      // 求出右边的最大值
      rightMax := 0
      for j := i + 1; j < len(height); j++ {
         rightMax = max(rightMax, height[j])
      }
      // 求出当前柱子能存的青豆
      sideHeight := min(leftMax, rightMax)
      // 当前柱子的高度小于两边的最大值,才能装青豆
      if height[i] < sideHeight {
         res += sideHeight - height[i]
      }
   }
   return res
}
func min(a, b int) int {
   if a < b {
      return a
   }
   return b
}
func max(a, b int) int {
   if a > b {
      return a
   }
   return b
}

暴力求解的时间复杂度为O(n^2),空间复杂度为O(1)。这种方法显然我们不能接受。

暴力求解优化:

从上面代码我们可以看到,在遍历每一个位置时我们去求它的左右最大值,导致在循环里重复很多次,优化是我们可以先求出每个位置的左右最大值,存在数组里,这样就不用循环遍历时去求取,也就是对数据进行预处理。

代码实现:

func trap(height []int) int {
   // 小于三个柱子,肯定不会有水,直接返回
   if len(height) <= 2 {
      return 0
   }
   res := 0
   // 用两个数组计算每个柱子左边的最大值和右边的最大值
   // 预处理要带上第一个和最后一个柱子,因为第一个和最后一个柱子有可能是最大值
   leftMax := make([]int, len(height))
   leftMax[0] = height[0]
   for i := 1; i < len(height); i++ {
      leftMax[i] = max(leftMax[i-1], height[i])
   }
   rightMax := make([]int, len(height))
   rightMax[len(height)-1] = height[len(height)-1]
   for i := len(height) - 2; i >= 0; i-- {
      rightMax[i] = max(rightMax[i+1], height[i])
   }
   // 第一根柱子和最后一根柱子一定不会有青豆,所以从第二根柱子开始遍历
   for i := 1; i < len(height)-1; i++ {
      sideHeight := min(leftMax[i], rightMax[i])
      // 当前柱子的高度小于两边的最大值,才能装青豆
      if sideHeight > height[i] {
         res += sideHeight - height[i]
      }
   }
   return res
}
func min(a, b int) int {
   if a < b {
      return a
   }
   return b
}
func max(a, b int) int {
   if a > b {
      return a
   }
   return b
}

优化后虽然空间复杂度变高了O(n),但时间复杂度低了O(n)

双指针

上面解法空间复杂度是O(n),那么我们能不能让空间复杂度降低呢?是可以的。上面解法它对于每个位置都找到了两边的最大值, 但实际上, 青豆数量是由两者中的较小值决定的, 所以只要找到较少的那个即可, 没必要找到较大的那个的最大值。我们只需要两个变量去记录leftMax和rightMax,在遍历时不断更新leftMax和rightMax,这样不用数组空间复杂度就降下来了。 具体解法就是left和right双指针从头和尾向中遍历,在遍历时更新leftMax和rightMax,当height[left] < height[right]时,right刚刚找到自己目前的最大值, left在寻找更大值的过程中。 所以此时必然有leftMax < rightMax, 这样就满足了我们寻找两者中较小者的要求. 所以就不用比较leftMax和rightMax。结果就是res += leftMax - height[left] 代码实现:

func trap(height []int) int {
   if len(height) <= 2 {
      return 0
   }
   res := 0
   // 左右两边的最大值
   leftMax, rightMax := 0, 0
   // left,right要从头和尾开始,因为第一根柱子或者最后一根柱子有可能是最大值
   left, right := 0, len(height)-1
   for left < right {
      // 更新最大值
      leftMax = max(leftMax, height[left])
      rightMax = max(rightMax, height[right])
      if height[left] < height[right] {
         // 此时一定满足leftMax < rightMax
         res += leftMax - height[left]
         left++
      } else {
         res += rightMax - height[right]
         right--
      }
   }
   return res
}
func max(a, b int) int {
   if a > b {
      return a
   }
   return b
}

时间复杂度O(n),空间复杂度O(1),应该是最优解了,至于其他题解不阐述了。