当青训营遇上码上掘金
码上掘金之接青豆
攒青豆
现有 n 个宽度为 1 的柱子,给出 n 个非负整数依次表示柱子的高度,排列后如下图所示,此时均匀从上空向下撒青豆,计算按此排列的柱子能接住多少青豆。(不考虑边角堆积)
Solution
看到这个题目我就绷不住了,经典的“接雨水”题目换了个皮肤。那么如何解决这个问题呢?
方案一
基础思路:我们单独考虑从1到N-2的所有竖向列,因为左右边界一定是不能用于接青豆的。
每一列能够承受的青豆数目,受制于它左边所有列中最高的和右边所有列中最高的列,左右两边最高的列决定了当前列能容纳的青豆数量。
因此使用leftMax[]数组和rightMax[]数组,分别记录每个位置上,左边列的最大值和右边列的最大值。通过一次从左向右的遍历和一次从右向左的遍历我们就可以分别得到这两个数组。
然后遍历所有列,每一列的青豆数就等于bean = min(leftMax[i-1], rightMax[i+1]) - height[i],当然,前提是这个数值为正数。
代码如下:
func catchBean1(height []int) int {
n := len(height)
lmax, rmax := make([]int, n), make([]int,n )
lmax[0] = height[0]
for i:= 1; i< n;i++{
lmax[i] = max(lmax[i-1], height[i])
}
rmax[n-1] = height[n-1]
for i:= n-2; i>=0; i--{
rmax[i] = max(rmax[i+1], height[i])
}
bean := 0
for i:= 1; i< n-1;i++{
b := min(lmax[i-1], rmax[i+1]) - height[i]
if b > 0{
bean += b
}
}
return bean
}
方案二
在方案一的基础上,考虑到最终我们求取青豆数量的时候,也是从左边向右边遍历的,并且每次我们只会用到当前位置上一个位置的leftMax值,所以对于leftMax的计算可以和最后一次遍历合并,总的遍历次数从3n降低到了2n,空间从2n降低到了n。
代码如下:
func catchBean2(height []int) int {
n := len(height)
rmax := make([]int,n )
rmax[n-1] = height[n-1]
for i:= n-2; i>=0; i--{
rmax[i] = max(rmax[i+1], height[i])
}
lmax := height[0]
bean := 0
for i:= 1; i< n-1;i++{
b := min(lmax, rmax[i+1]) - height[i]
if b > 0{
bean += b
}
lmax = max(lmax, height[i])
}
return bean
}
方案三
在上一个方案的基础上,如果能把右侧的rightMax也优化掉,那么就可以将遍历次数进一步降低。我们可以考虑使用两个指针来维护当前状态。
考虑一个right,一个left指针,那么如果当前的left比right小,那么我们如果移动right,高度总是受限于left的,而实际上中间可能存在比left更高的柱子。因此这种情况下需要移动left,同理可以得到left比right大的情况。
这就让整体的时间复杂度到了n,而空间复杂度变成了常数。
代码如下:
func catchBean3(height []int) int {
n := len(height)
beans := 0
left, right := 0, n-1
lmax, rmax := height[0], height[n-1]
for left < right{
lmax = max(lmax, height[left])
rmax = max(rmax, height[right])
if height[left] < height[right]{
beans += lmax - height[left]
left++
}else{
beans += rmax - height[right]
right--
}
}
return beans
}
总地来说,这是一个很经典的算法题目,使用上述的方法,层层递进地减少时间和空间地消耗,在思想上也是连贯的。当然,除了上述方法之外还有其他解法,在这里就不描述了。