当青训营遇上码上掘金
在本次青训营 X 码上掘金主题活动中,我选择了主题4:攒青豆。
首先我们来看一下攒青豆的题目要求:现有 n 个宽度为 1 的柱子,给出 n 个非负整数依次表示柱子的高度,排列后如下图所示,此时均匀从上空向下撒青豆,计算按此排列的柱子能接住多少青豆。(不考虑边角堆积)
这道题的意思实际上就是求n个宽度为1的柱子中间区域的面积。
遇到没有思路的算法题,我们首先考虑暴力求解。对任何一点,要计算它能够接多少青豆,可以直观地看到这点能接的青豆是在该点的青豆最大高度 - 该点当前高度,即:
当前高度已经给出,问题就是在该点,青豆能堆叠到的最大高度是多少?经过观察后,我们可以发现,在某一点往左扫描的最大高度和往右扫描的最大高度可以形成一个夹缝,将中间的某个点包围起来以接到青豆。显然,如果取这两端的最大值,青豆就会“漏出来”,只有取两端的最小值,才能接住青豆。因此,该点能接到青豆的最大高度就是往左扫描最大高度和往右扫描最大高度的最小值,即:
以下图为例:
如果要计算第三列(柱子高度为2)能接的青豆数量,分别向左向右扫描,发现最大值的两端点是5和4,取其小值4,则在该点能接到的青豆数量就是4 - 2 = 2个。
这样,分别计算每个点能接的青豆数量,就能得到能接的总青豆数量了。这种计算方法需要分别扫描每个点,并在每个点处向左向右进行一次扫描,时间复杂度为O(n^2)。
为了优化这个算法,我们想到:如果能动态更新左右的最大值,不用再每次扫描到某个点时再扫描其左右最大值,而是在一次扫描中找到某个点向左/右的最大值,就能把时间复杂度优化为O(n)。这种优化方式属于动态规划。
首先,最左的点向左的最大值以及最右的点向右的最大值一定是本身。对于其他点,向左的的最大值可以通过前一个点的最大值和当前值进行对比取大值;向右的最大值可以通过后一个点的最大值和当前值对比取最大值。将每列的高度写为height,其长度为n,向左的最大值为leftMax,向右的最大值为rightMax,其状态转移方程如下:
这样,对height数组进行两次扫描,即可获得所有点向左向右的最大值,即两端,之后按照之前的解题思路即可算出能接到的青豆数量总数。这种算法的时间复杂度为O(n)。
Go实现攒青豆的代码如下:
package main
import "fmt"
func min(x, y int64) int64 {
if x < y {
return x
}
return y
}
func max(x, y int64) int64 {
if x > y {
return x
}
return y
}
func main() {
const N = 100010
var height [N]int64 //每列的柱子高度
var n int //假设共有n列柱子
fmt.Print("输入有共有几列柱子:")
fmt.Scanf("%d\n", &n)
fmt.Print("输入每列的柱子长度:")
for i := 0; i < n; i++ {
fmt.Scanf("%d", &height[i])
}
var ans int64
ans = 0
//创造每个点左边/右边(包括自身)的最大值数组
var leftMax [N]int64
var rightMax [N]int64
//计算leftMax
leftMax[0] = height[0]
rightMax[n-1] = height[n-1]
//通过dp动态更新leftMax数组的值
for i := 1; i < n; i++ {
leftMax[i] = max(leftMax[i-1], height[i])
}
//rightMax同上6
for i := n - 2; i >= 0; i-- {
rightMax[i] = max(rightMax[i+1], height[i])
}
//在某点处能接的青豆的数量就是左边最大和右边最大的较小值减去本身高度,将这些青豆数量求和
for i := 0; i < n; i++ {
ans += min(leftMax[i], rightMax[i]) - height[i]
}
fmt.Println(ans)
}
运行结果:
以上就是我对攒青豆的解题方法分析,本人水平有限,如有错误欢迎指正。