当青训营遇上码上掘金
本次选择的主题是:
-
主题 4:攒青豆
现有 n 个宽度为 1 的柱子,给出 n 个非负整数依次表示柱子的高度,排列后如下图所示,此时均匀从上空向下撒青豆,计算按此排列的柱子能接住多少青豆。(不考虑边角堆积)
读题,可以看出给出的是每个柱子的高度,按照这种柱子构造求能够接住最多多少青豆。观察图片,可以发现,每个柱子所能承载的豆子数量取决于这个柱子的高度,以及这个柱子左右两边较矮的那根柱子。
于是就有两种计算思路,一种是按照行来,一行一行的计算豆子的数量,有点类似于一层一层的铺满,通过求每行所能承载的豆子数量来计算最大的豆子承载量。另一种是按列来进行计算,一列一列的计算豆子的数量。
由于按列计算比较易于理解与实现,文章中所实现的方法也是按列进行计算,分别用了常见的两种方法来进行实现:动态规划法以及双指针法。
动态规划
根据图片所示进行理解,可以发现每一列所能积攒的豆子数取决于该列左右的最高柱子。根据木桶效应,可以理解,积攒的豆子数量取决于更低的那根柱子。同时,若是该列能够积攒豆子,还需要该列的高度比左右最高的柱子都矮才行,否则无法积攒。
根据分析可得,可以通过动态规划分别获取第i列的左端最高柱子以及右端最高柱子。
设左端最高柱子动态规划数组,由于左侧的最高柱子取决于下标在前的前一个最高柱子,因此状态转移方程为:
leftMaxHeight[i] = max(height[i],leftMaxHeight[i-1])
//dp初始化
leftMaxHeight := make([]int, len(height))
leftMaxHeight[0] = height[0]
同理,最高右端柱子需要从大的下标逐个往小遍历,状态转移方程为:
rightMaxHeight[i] = max(height[i],rightMaxHeight[i+1])
初始化为:
//dp初始化
leftMaxHeight := make([]int, len(height))
leftMaxHeight[0] = height[0]
在计算完每个下标的左右最大柱子高度后,计算每一列所能积攒的豆子数,逻辑较为简单,代码如下:
for i := 1; i < len(height)-1; i++ {
h := -height[i]
if leftMaxHeight[i] < rightMaxHeight[i] {
h += leftMaxHeight[i]
} else {
h += rightMaxHeight[i]
}
if h > 0 {
maxBeans += h
}
}
详细代码实现请参考,点击运行即可获得测试样例结果:
双指针法
在讲完了易于理解的动态规划后,我们来使用双指针法进行求解。前面也说明了关键在于求解每一列的最大的左端柱子高度以及最大的右端数组长度,有什么办法可以不需要经过多次循环就能获得上述所需的参数呢?这里就要用上双指针法。首先我们定义双指针的左侧和右侧下标,并定义在当前指针状态的最大左侧柱子长度以及最大右侧柱子长度:
left, right := 0, len(height)-1
maxLeft, maxRight := 0, 0
像常规的双指针一样,需要定义循环,在left小于right的情况下执行循环代码块里的内容。
首先判断左右指针对应的柱子长度的大小,假设height[left]<height[right],同时height[left]<maxLeft,那么说明left下标所在的柱子形成了一个可以积攒豆子的空间,大小为maxLeft-height[left]。同理可得height[right]<height[left]的结果,最终代码见以下代码:
func CaluBeansDoublePoints(height []int) int {
left, right := 0, len(height)-1
maxLeft, maxRight := 0, 0
maxBeans := 0
for left < right {
if height[left] < height[right] {
if height[left] >= maxLeft {
maxLeft = height[left]
} else {
maxBeans += maxLeft - height[left]
}
left++
} else {
if height[right] > maxRight {
maxRight = height[right]
} else {
maxBeans += maxRight - height[right]
}
right--
}
}
return maxBeans
}