「青训营 X 码上掘金」攒青豆

112 阅读3分钟

当青训营遇上码上掘金

1、任务介绍

现有 n 个宽度为 1 的柱子,给出 n 个非负整数依次表示柱子的高度,排列后如下图所示,此时均匀从上空向下撒青豆,计算按此排列的柱子能接住多少青豆。(不考虑边角堆积) image.png 以上图为例,输入为每根柱子的高度即height=[5,0,2,1,4,0,1,0,3],输出就为能够接住的青豆数量即num=17

2、任务分析及编码

2.1 按列计算

最简单直接的办法就是计算每列能够攒住的青豆数量,而每列的青豆数量只需要关注当前列左右两边的最高柱子就可以了。根据木桶效应,每列能接住的青豆数量与左边或右边最高柱子中最矮的那个有关。 picture.png 如上图所示,当前要计算第三列的青豆数,左边最高列是5,右边最高列是4。则当前列的青豆数只要用左右两边最高列中较小的高度减去当前列的高度就可以了,即min(5,4)-2=2

如下为按列计算的代码函数,首先遍历整个高度数组,由于第一列和最后一列一定不会接到青豆,所以遍历从第二列到倒数第二列为止。每次遍历时要分别向当前列左边和右边遍历数组,找到左边和右边的最高列,再根据每列的青豆数量公式可以得到该列的青豆数,最后将每列的青豆数相加就是能够接住的总青豆数量。

//按列计算
func trap1(height []int) int {
	sum := 0
	for i := 1; i < len(height)-1; i++ {
		max_left := 0
		max_right := 0        
		for j := i - 1; j >= 0; j-- {   
                //左边最高列高度
			if height[j] > max_left {
				max_left = height[j]
			}
		}
		for j := i + 1; j < len(height); j++ {
                //右边最高列高度
			if height[j] > max_right {
				max_right = height[j]
			}
		}
		if min(max_left, max_right) > height[i] {
			sum += min(max_right, max_left) - height[i]
		}
	}
	return sum
}

使用该方法的时间复杂度是O(n²),遍历每一列需要 n,找出左边最高和右边最高的墙加起来刚好又是一个 n,所以是 n²。空间复杂度是O(1)。

2.2 动态规划

前面解法中,再求每列的左边和右边最高柱子时,都是重新遍历一遍所有的高度,我们可以在这里优化一下。

首先,定义两个数组Left_max[],Right_max[]Left_max[i]表示第i列左边最高的柱子,Right_max[i]表示第i列右边最高的柱子。

对于Left_max[i]我们可以直接从头开始遍历,每次遍历只用当前柱子高度和前一个柱子高度相比,最高的柱子就是当前列的左边最高列。

同样对于Right_max[],从末尾遍历到开头,每次遍历只用当前柱子高度和后一个柱子高度比较,最高的柱子就是该列右边最高列。

这样就直接得到了每列左右两边的最高列,而不用每次遍历再去遍历一遍数组找到最高列。

// 动态规划
func trap2(height []int) int {
	sum := 0
	Left_max := make([]int, len(height))
	Right_max := make([]int, len(height))
	for i := 1; i < len(height); i++ {
		Left_max[i] = max(Left_max[i-1], height[i-1])
	}
	for i := len(height) - 2; i >= 0; i-- {
		Right_max[i] = max(Right_max[i+1], height[i+1])
	}
	for i := 1; i < len(height)-1; i++ {
		h := min(Left_max[i], Right_max[i]) - height[i]
		if h > 0 {
			sum += h
		}
	}

	return sum
}

这种方法的时间复杂度是O(n),但空间复杂度是O(n),因为多使用内存用于保存每列左右两边的最高列。与前一种方法相比,这种方法牺牲空间复杂度来降低时间复杂度。

3.总结

本题是一道很经典的算法题,除了上述介绍的两种方法,本题还有许多更优的解法,如双指针,单调栈等,而上述两种方法个人认为是最好理解的。