当青训营遇上码上掘金
最近参加了字节跳动第五届青训营,在「青训营 X 码上掘金」主题创作活动中,遇到了一道有趣的算法题攒青豆,这里分享一些解题过程。
- 题目:现有 n 个宽度为 1 的柱子,给出 n 个非负整数依次表示柱子的高度,排列后如下图所示,此时均匀从上空向下撒青豆,计算按此排列的柱子能接住多少青豆。(不考虑边角堆积)
看到这道题目,相信经常在leetcode刷题的朋友们都能下意识的找到一道相似的题目,正是接雨水问题。42. 接雨水 - 力扣(Leetcode)
那么这道题应该如何分析解决呢?
首先,根据图片我们可以分析,从列上看,由于每一列的宽度为1,因此我们只需要找到每一个凹槽的高度,我们就可以得到该列能攒到的青豆了。如下图所示:
对于某一列来说,我们要获取该列上的凹槽高度,首先要想到的就是短板效应,即首先我们应该找到该列左右两边最短的柱子高度,然后将最短高度减去该列上柱子的高度,就是凹槽的高度了。即:
h = min(lHeight,rHeight)-height[i] // lHeight,rHeight分别为第i列左边最高柱子高度与第i列右边柱子的最大高度。
上面的公式是根据一般情况推导出来的,那么其是否适合所有情况呢?我们分别代到可能出现的情况去验证一下:
-
某一列上没有柱子
用上图示例中的第二列来验证,对于第二列,lHeight=5,rHeight=4,因此该列的凹槽高度为min(lHeight,rHeight)-0=4,因此该列上能攒到4青豆。验证无误。 -
某一列上有柱子但没有凹槽
用上图示例中的第5列(即高度为4的柱子)来验证,对于该列,lHeight=5,rHeight=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时,都要向左和向右去进行很多次无意义的重复遍历来获取lHeight和rHeight。那么我们有没有办法在某个方向上只需要遍历一次,就能获取每一列在该方向上的最大值呢?
这时候我们就可以引用动态规划了,通过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
}