当青训营遇上码上掘金
攒青豆这个题目,不得不说是充满了既视感。作为一名acm选手,三种做法在一开始就 come into my mind。
主题介绍
现有 n 个宽度为 1 的柱子,给出 n 个非负整数依次表示柱子的高度,排列后如下图所示,此时均匀从上空向下撒青豆,计算按此排列的柱子能接住多少青豆。(不考虑边角堆积)
解法介绍
暴力模拟
这应该是大家都能够想到的一种方法。按照每一列来计算能够攒下的最多的青豆数,我们只需要关注左右最高的柱子高度和当前的柱子高度,然后进行分类讨论。遍历每一列需要次,而每一列都要找到左右最高的柱子,暴力去找又是次,时间复杂度。
动态规划
暴力的时间复杂度显然不太能够接受,于是就来到了我们又爱又恨的动态规划。而动态规划的关键,就在于优化寻找左右最高柱子的这个过程。
我们可以开两个数组分别存储对于柱子来说,左右最高的柱子分别有多高。这样的话,状态转移方程也很好想,在扫描所有列的同时进行更新即可。时间复杂度。
在优化寻找左右最值这个思路上,我们可以想到使用单调栈这样的数据结构,因为单调栈正好是“找到左右两边第一个比当前数大/小的数”的数据结构,这里就不展开叙述了。
双指针
双指针解法实际上是对动态规划解法的空间优化。可以发现动态规划数组保存的左右柱子高度只会被使用一次,之后便不再使用了。所以不用保存下来,我们可以用双指针去动态推出需要的值。
package main
import "fmt"
func main() {
height := []int{5, 0, 2, 1, 4, 0, 1, 0, 3}
fmt.Println(solve(height))
}
func solve(height []int) int {
min := func(a, b int) int {
if a < b {
return a
} else {
return b
}
}
if len(height) == 1 {
return 0
}
l, r := 0, len(height)-1
lM, rM := height[l], height[r]
container := (r - l - 1) * min(lM, rM)
for l < r {
if height[l] <= height[r] {
l++
if l >= r {
break
}
if height[l] < lM {
container -= height[l]
} else {
lC := container - lM*(r-l)
rC := (r - l - 1) * min(height[l], height[r])
container = lC + rC
lM = height[l]
}
} else {
r--
if l >= r {
break
}
if height[r] < rM {
container -= height[r]
} else if height[r] >= rM {
rC := container - rM*(r-l)
lC := (r - l - 1) * min(height[l], height[r])
container = rC + lC
rM = height[r]
}
}
}
return container
}
结语
这道题目是一道经典的好题。从暴力的做法,一步步自然地优化从而得到一个比较好的解,对于提高对动态规划、数据结构的认识有积极的帮助。