「青训营 X 码上掘金」主题创作活动-攒青豆

60 阅读3分钟

当青训营遇上码上掘金

最近参加了字节跳动第五届青训营,在「青训营 X 码上掘金」主题创作活动中,遇到了一道有趣的算法题攒青豆,这里分享一些解题过程。

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

image.png
看到这道题目,相信经常在leetcode刷题的朋友们都能下意识的找到一道相似的题目,正是接雨水问题。42. 接雨水 - 力扣(Leetcode)
那么这道题应该如何分析解决呢?
首先,根据图片我们可以分析,从列上看,由于每一列的宽度为1,因此我们只需要找到每一个凹槽的高度,我们就可以得到该列能攒到的青豆了。如下图所示:

c284b7c6d0ff3d0cbb0a2805e399dc5.jpg
对于某一列来说,我们要获取该列上的凹槽高度,首先要想到的就是短板效应,即首先我们应该找到该列左右两边最短的柱子高度,然后将最短高度减去该列上柱子的高度,就是凹槽的高度了。即:

    h = min(lHeight,rHeight)-height[i] // lHeight,rHeight分别为第i列左边最高柱子高度与第i列右边柱子的最大高度。

上面的公式是根据一般情况推导出来的,那么其是否适合所有情况呢?我们分别代到可能出现的情况去验证一下:

  • 某一列上没有柱子
    用上图示例中的第二列来验证,对于第二列,lHeight=5,rHeight=4,因此该列的凹槽高度为min(lHeight,rHeight)-0=4,因此该列上能攒到4青豆。验证无误。

  • 某一列上有柱子但没有凹槽
    用上图示例中的第5列(即高度为4的柱子)来验证,对于该列,lHeight=5rHeight=4(最大高度的柱子包括本身),因此,h=min(lHeight,rHeight)-height[i] = 5 - 4 - 4 = -3 < 0 。此时,该列上无法攒青豆

  • 第一列与最后一列
    对于第一列与最后一列,其本身就不能攒青豆,其lHeight=rHeight=height[i],因此,h=min(lHeight,rHeight)-Height[i]=0,此时该列上也无法攒青豆。

    综上所述,对于h=min(lHeight,rHeight)-Height[i],当h>0时,h就是当前列能攒的青豆,当h<=0时,当前列就无法攒青豆。

    因此,这道题最终转换为了找到数组中第i个位置的左边最大值lHeight和右边的最大值rHeight。为此,我想到了两种方法来解决。

    双指针遍历法。

    既然我们要找第i个位置左边和右边的最大值,我们最容易想到的就是暴力遍历了。我们可以使用两个指针,分别从第i个位置向左和向右依次遍历,找到其中的最大值即可。具体代码如下:

// 双指针
func collectGreenBeens1(height []int)int{
  size := len(height)
  if size <= 2{ // 如果只有头尾柱子,无法形成凹槽,攒不了青豆
    return 0
  }
  ans := 0
  for i := 0; i < size; i++{ //遍历每一列
    if i == 0 || i == size - 1{ // 头尾柱子无法攒青豆,直接跳过
      continue
    }

    lHeight,rHeight := height[i],height[i] //双指针从本列出发

    for l := i-1; l >= 0;l--{ // 寻找左边柱子的最大值
      lHeight = max(height[l],lHeight)
    }

    for r := i+1; r < size;r++{ // 寻找右边柱子的最大值
      rHeight = max(height[r],rHeight)
    }

    h := min(lHeight,rHeight) - height[i] // 计算凹槽的高度
    if h > 0{ // 当h > 0时,h即为该列能攒的青豆数
      ans += h
    }

  }
  return ans
}

动态规划法

上述的双指针法中,我们可以发现一个很大的缺点,就是每此计算新一列的凹槽高度h时,都要向左和向右去进行很多次无意义的重复遍历来获取lHeightrHeight。那么我们有没有办法在某个方向上只需要遍历一次,就能获取每一列在该方向上的最大值呢?
这时候我们就可以引用动态规划了,通过dp数组来记录每一列在某个方向上的最大值,当遍历到新列时,我们只要到dp数组中去比较上一列的最大值即可,就无需多次重复遍历了,代码如下:

// 动态规划
func collectGreenBeens2(height []int)int{
  if len(height) <= 2{
    return 0
  }
  
  size := len(height)
  maxLeft := make([]int,size) //dp数组,用于记录第i列左侧最大高度
  maxRight := make([]int,size) //dp数组,用于记录第i列右侧最大高度

  // 更新maxLeft
  maxLeft[0] = height[0]
  for i := 1;i < size; i++{
    maxLeft[i] = max(height[i],maxLeft[i-1])
  }

  // 更新maxRight
  maxRight[size-1] = height[size-1]
  for i := size-2;i >= 0; i--{
    maxRight[i] = max(height[i],maxRight[i+1])
  }

  ans := 0
  // 计算青豆数
  for i := 0; i < size;i++{
    h := min(maxLeft[i],maxRight[i])-height[i]
    if h > 0{
      ans += h 
    }
  }
  return ans
}