LeetCode 42.接雨水【困难】

173 阅读4分钟

题干

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。 示例 1:

输入: height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出: 6
解释: 上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 

示例 2:

输入: height = [4,2,0,3,2,5]
输出: 9

题解

可以装的水的总量为每个点能装的数量的和,问题在于每个点能装的水量如何计算。想象一下一个点能装水,那它一定在一个坑里面,也就是说该点的左边有比它高的点,它的右边也有比它高的点。我们设置该点及该点左边最大高度为leftMax[i],该点及该点右边最大的高度为rightMax[i],那么显然这个点能装多少水是根据这两个值的最小值决定的,因此可以得出min(leftMax[i], rightMax[i]) - height[i]就是该点可以装的水量。为了得到leftMaxrightMax,我们可以从左边、右边开始各遍历一次得到,最后再遍历一次计算得到每个点的水量并求和。

func trap(height []int) int {
	n := len(height)
	// leftMax存储某个点及其左侧的最大高度、rightMax存储某个点及其右侧的最大高度
	leftMax := make([]int, n)
	rightMax := make([]int, n)
	leftMax[0] = height[0]
	for i := 1; i < n; i++ {
		leftMax[i] = max(leftMax[i-1], height[i])
	}
	rightMax[n-1] = height[n-1]
	for i := n - 2; i >= 0; i-- {
		rightMax[i] = max(rightMax[i+1], height[i])
	}

	// 某个点能装的水量是该点两边最大高度的最小值减去当前点的高度
	ans := 0
	for i := 0; i < n; i++ {
		ans += min(leftMax[i], rightMax[i]) - height[i]
	}
	return ans
}

上述算法的时间复杂度为O(n),空间复杂度为O(n)。这个方法其实是动态规划的思想,既然是动态规划,就可以尝试优化它的空间复杂度。上面的方法中,我们使用了leftMaxrightMax两个数组来存放各个点左右两边的最大高度,可以使用两个滚动的变量来存放当前的左侧最大高度leftMax和右侧的最大高度rightMax,并在循环过程中不断更新这两个值,这样空间复杂度就是O(1)了。具体步骤如下:

  • 设置两个指针leftright,左指针位于数组左侧,右指针位于数组右侧,并随着循环向中间移动
  • 每次循环计算leftMaxrightMax,如果leftMax < rightMax,那么当前可装水量为leftMax - height[i],然后将left向右侧移动;反之,当前可装水量为rightMax - height[i],然后将right向左侧移动。
  • 如此循环直到左右指针重合,返回总水量
func trap(height []int) int {
	n := len(height)
	// leftMax,rightMax分别代表当前的左右最大值,在循环中滚动更新
	// 初始值分别为数组第一个值和最后一个值
	leftMax, rightMax := height[0], height[n-1]
	// 左右指针,初始值位于数组两端
	left, right := 0, n-1
	ans := 0
	for left < right {
		leftMax = max(leftMax, height[left])
		rightMax = max(rightMax, height[right])
		// 如果leftMax < rightMax,根据leftMax计算水量,左指针向中间移动
		// 如果leftMax >= rightMax, 根据rightMax计算水量,右指针向中间移动
		if leftMax < rightMax {
			ans += leftMax - height[left]
			left++
		} else {
			ans += rightMax - height[right]
			right--
		}
	}
	return ans
}

一开始看到这个算法,第一个反应是这不是全局最优解,只是局部最优解,但实际上不是这样的,下面我们来论证它为什么是正确的。我们记在某个点i,其左侧的最大值为leftMaxGlobal[i],其右侧的最大值为leftMaxGlobal[i]。假设在程序运行的某个状态,左指针指向left,右指针指向right,现在要从这两个指针中选择一个可以确定水量的指针,从代码中我们可以看出当leftMax < rightMax时,可以确定left位置的水量;反之可以确定right位置的水量。当leftMax < rightMax,对于left来说,leftMaxGlobal[left] = leftMax[left]rightMaxGlobal[left] >= rightMax[right],当leftMax[left] < rightMax[right]时,就是leftMaxGlobal[left] = leftMax[left] < rightMax[right] <= rightMaxGlobal[left],也即leftMaxGlobal[left] < rightMaxGlobal[left],满足min(leftMax[i], rightMax[i]) - height[i]公式,我们可以确定left位置的水量;反之,当leftMax[left] >= rightMax[right]时,就有leftMaxGlobal[right] >= leftMax[left] >= rightMax[right] = rightMaxGlobal[right],也即leftMaxGlobal[right] >= rightMaxGlobal[right],同样满足公式,我们可以确定right位置的水量。综上所述,当leftMax[left] < rightMax[right]时,一定满足leftMaxGlobal[left] < rightMaxGlobal[left],我们应该计算left的水量,计算完了之后将left右移;当leftMax[left] >= rightMax[right]时,一定满足leftMaxGlobal[right] >= rightMaxGlobal[right],我们应该计算right的水量,计算完了之后将right左移。该方法的时间复杂度为O(n),空间复杂度优化到了O(1)。