这个寒假,青豆相伴

14 阅读2分钟

当青训营遇上码上掘金

攒青豆这个题目,不得不说是充满了既视感。作为一名acm选手,三种做法在一开始就 come into my mind。

主题介绍

现有 n 个宽度为 1 的柱子,给出 n 个非负整数依次表示柱子的高度,排列后如下图所示,此时均匀从上空向下撒青豆,计算按此排列的柱子能接住多少青豆。(不考虑边角堆积)

解法介绍

暴力模拟

这应该是大家都能够想到的一种方法。按照每一列来计算能够攒下的最多的青豆数,我们只需要关注左右最高的柱子高度和当前的柱子高度,然后进行分类讨论。遍历每一列需要nn次,而每一列都要找到左右最高的柱子,暴力去找又是nn次,时间复杂度O(n2)O(n^2)

动态规划

暴力的时间复杂度显然不太能够接受,于是就来到了我们又爱又恨的动态规划。而动态规划的关键,就在于优化寻找左右最高柱子的这个过程。

我们可以开两个数组分别存储对于柱子ii来说,左右最高的柱子分别有多高。这样的话,状态转移方程也很好想,在扫描所有列的同时进行更新即可。时间复杂度O(n)O(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
}

结语

这道题目是一道经典的好题。从暴力的做法,一步步自然地优化从而得到一个比较好的解,对于提高对动态规划、数据结构的认识有积极的帮助。