当青训营遇上码上掘金
主题 4:攒青豆
题目
现有 n 个宽度为 1 的柱子,给出 n 个非负整数依次表示柱子的高度,排列后如下图所示,此时均匀从上空向下撒青豆,计算按此排列的柱子能接住多少青豆。(不考虑边角堆积)
以下为上图例子的解析:
输入:height = [5,0,2,1,4,0,1,0,3]
输出:17
解析:上面是由数组 [5,0,2,1,4,0,1,0,3] 表示的柱子高度,在这种情况下,可以接 17 单位的青豆。
解法
这道题是典型的接雨水问题,我们有两种思路,三种解法。方法一二属于同一种思路,计算每一个下标处可以接的青豆数,只需要考虑高度;方法三的思路是横向切片,需要同时考虑宽度和高度。
方法一:前后缀分解
朴素的想法是,对于第i个柱子,分别向左右遍历找左侧最大高度和右侧最大高度,计算能接的青豆数。这样做时间复杂度是O(n^2)。
优化思路:预先用两个数组preMax,sufMax记录第i个柱子左侧最大高度和右侧最大高度。最后再遍历一遍height数组计算每个单位能接的青豆数
实现细节:前缀和后缀包含height[i]本身,这样可以避免判断min(preMax[i], sufMax[i]) - h <0的情况,写法上也会更加简单
func solution1(height []int) int{//前后缀分解:预先计算每个柱子左右最大高度,依次计算第i个柱子处能接的青豆
n := len(height)
ans := 0
preMax := make([]int, n)
preMax[0] = height[0]
for i := 1; i < n; i++ {
preMax[i] = max(preMax[i-1], height[i])
}
sufMax := make([]int, n)
sufMax[n-1] = height[n-1]
for i := n - 2; i >= 0; i-- {
sufMax[i] = max(sufMax[i+1], height[i])
}
for i, h := range height {
ans += min(preMax[i], sufMax[i]) - h
}
return ans
}
时间复杂度:O(n)
空间复杂度:O(n)
方法二:同向双指针
本方法是对方法一的优化,可将空间复杂度降至O(1)。
优化思路:假设现在有左指针left和右指针right,我们可以知道左指针左边的最大高度pre和右指针右边的最大高度suf,选二者中小的值的指针,则该指针处可装载的青豆数是确定的
func solution2(height []int) int {//相向双指针,优化空间复杂度
n := len(height)
left,right,pre,suf,ans := 0,n-1,0,0,0
for left<right{
pre = max(height[left],pre)
suf = max(height[right],suf)
if pre < suf{
ans += pre - height[left]
left ++
}else{
ans += suf - height[right]
right --
}
}
return ans
}
时间复杂度:O(n)
空间复杂度:O(1)
方法三:单调栈
遍历height维护一个单调递减的单调栈。栈内至少有两个元素,当遍历元素大于栈顶元素top时,弹出top,然后与top的下一元素left进行比较,如果height[i]>height[left]则得到一个长为i - left - 1,高为min(height[left], height[i]) - height[top]的可攒青豆区域。遍历完成后得到能接的青豆总量。
func solution3(height []int) int {//单调栈:分区域计算青豆数
stack := []int{}
ans := 0
for i, h := range height {
for len(stack) > 0 && h > height[stack[len(stack)-1]] {
top := stack[len(stack)-1]
stack = stack[:len(stack)-1]
if len(stack) == 0 {
break
}
left := stack[len(stack)-1]
curWidth := i - left - 1
curHeight := min(height[left], h) - height[top]
ans += curWidth * curHeight
}
stack = append(stack, i)
}
return ans
}
时间复杂度:O(n)
空间复杂度:O(n)