当青训营遇上码上掘金
主题 4:攒青豆
现有 n 个宽度为 1 的柱子,给出 n 个非负整数依次表示柱子的高度,排列后如下图所示,此时均匀从上空向下撒青豆,计算按此排列的柱子能接住多少青豆。(不考虑边角堆积)
在解题前,先思考一个问题,什么决定了每一列的攒青豆的个数?
显然只有当一列与两侧构成“凹”字型的结构时,才可以攒下青豆,而且攒下青豆的最大高度取决于两侧的最大高度。而该列攒下的青豆数就为两侧最大高度的较小者减去自身的高度。
通过上述分析,问题可以转换为求每一列两侧的最大高度为多少。
暴力解法是遍历给定高度数组中的每一列,分别向左和向右暴力扫描并记录左边和右边的最大高度,然后计算每个下标位置能攒下的青豆数。假设共有 列,该做法共需要 次对每个下标位置使用 的时间向两边扫描并得到最大高度,因此暴力解法总时间复杂度是 。
如何优化求两侧的最大高度?可以发现暴力扫描去求解时,有许多信息被重复计算,往往这种信息重复计算的情况下,首先想到的应该是如何去保存已经计算过的信息,后续计算直接使用去避免重复计算。
首先为了减少计算攒青豆数的情况讨论,我们求两侧的最大高度时需要包括当前列。求一侧的最大高度可以利用动态规划求解,用两个辅助数组记录每一列求得得两侧最大高度,以左侧为例,转移方程为:
右侧同理,我们可以通过两次动态规划求出两侧的最大高度。有了两侧的最大高度,就可以求得每一列能攒下的青豆数,再求和就可以求得一共能攒下的青豆数。
时间复杂度:
空间复杂度:使用了两个辅助数组
具体代码实现如下:
package main
import (
"bufio"
. "fmt"
"io"
"os"
)
/**
测试样例
输入:
9
5 0 2 1 4 0 1 0 3
输出:
17
主题 4:攒青豆
动态规划解法:从左往右,从右往左两次dp,
每个位置能攒下的青豆数为左右两侧最大高度的较小值减去自身高度
*/
func main() { solve(os.Stdin, os.Stdout) }
func solve(_r io.Reader, _w io.Writer) {
in := bufio.NewReader(_r)
out := bufio.NewWriter(_w)
defer out.Flush()
var n int
Fscan(in, &n)
height := make([]int, n)
for i := 0; i < n; i++ {
Fscan(in, &height[i])
}
leftMax := make([]int, n)
leftMax[0] = height[0]
for i := 1; i < n; i++ {
leftMax[i] = max(leftMax[i-1], height[i])
}
rightMax := make([]int, n)
rightMax[n-1] = height[n-1]
for i := n - 2; i >= 0; i-- {
rightMax[i] = max(rightMax[i+1], height[i])
}
ans := 0
for i, h := range height {
ans += min(leftMax[i], rightMax[i]) - h
}
Fprintln(out, ans)
}
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
}