LeetCode 接雨水问题详解 - 动态规划解法
目录
问题描述
解法思路:动态规划
核心思想
步骤解析
代码实现
复杂度分析
示例解析
画图解析
较矮的墙的高度大于当前列的墙的高度
较矮的墙的高度小于当前列的墙的高度
较矮的墙的高度等于当前列的墙的高度。
总结
问题描述
给定一个非负整数数组 height
,每个元素表示柱子的高度。假设这些柱子排列紧密,计算下雨后这些柱子之间能接多少雨水。例如,输入 [0,1,0,2,1,0,1,3,2,1,2,1]
,输出为 6。
解法思路:动态规划
核心思想
每个位置能接的雨水量取决于其左右两边最高墙中的较矮者。若较矮的墙高于当前柱子,则积水量为两者之差;否则无法积水。
步骤解析
-
预处理左右最高墙:
-
left[i]
:记录位置i
左边的最高墙(不包含i
)。 -
right[i]
:记录位置i
右边的最高墙(不包含i
)。
-
-
计算积水量:遍历每个位置,取左右最高墙中的较小值,减去当前高度,累加到结果中。
代码实现
/**
* @param {number[]} height
* @return {number}
*/
var trap = function (height) {
let n = height.length;
let left = new Array(n).fill(0); // 左边最高的墙
let right = new Array(n).fill(0); // 右边最高的墙
for (let i = 1; i < n; i++) {
// 从左到右遍历,记录每个位置左边最高的墙
left[i] = Math.max(left[i - 1], height[i - 1]);
}
for (let i = n - 2; i >= 0; i--) {
// 从右到左遍历,记录每个位置右边最高的墙
right[i] = Math.max(right[i + 1], height[i + 1]);
}
console.log(left, right);
let ans = 0;
for (let i = 0; i < n; i++) {
// 遍历每个位置,计算每个位置能接的水
let level = Math.min(left[i], right[i]);
//只有较小的一段大于当前列的高度才会有水,其他情况不会有水
if (level > height[i]) {
// 接水 = 较矮的一边减去当前位置的高度
ans += level - height[i];
}
}
return ans;
};
console.log(trap([0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1]));
复杂度分析
-
时间复杂度:O(n),三次遍历数组。
-
空间复杂度:O(n),存储左右最高墙的数组。
示例解析
以输入 [0,1,0,2,1,0,1,3,2,1,2,1]
为例:
-
预处理左右数组:
-
left = [0,0,1,1,2,2,2,2,3,3,3,3]
-
right = [3,3,3,3,3,3,3,2,2,2,1,0]
-
-
计算积水量:
-
对于位置
i=2
,left[2]=1
,right[2]=3
,较矮者为1。当前高度为0,积水1。 -
其他位置类似,累加后总积水量为6。
-
画图解析
求每一列的水,我们只需要关注当前列,以及左边最高的墙,右边最高的墙就够了。
装水的多少,当然根据木桶效应,我们只需要看左边最高的墙和右边最高的墙中较矮的一个就够了。
所以,根据较矮的那个墙和当前列的墙的高度可以分为三种情况。
较矮的墙的高度大于当前列的墙的高度
把正在求的列左边最高的墙和右边最高的墙确定后,然后为了方便理解,我们把无关的墙去掉。
这样就很清楚了,现在想象一下,往两边最高的墙之间注水。正在求的列会有多少水?
很明显,较矮的一边,也就是左边的墙的高度,减去当前列的高度就可以了,也就是 2 - 1 = 1,可以存一个单位的水。
较矮的墙的高度小于当前列的墙的高度
同样的,我们把其他无关的列去掉。
想象下,往两边最高的墙之间注水。正在求的列会有多少水?
正在求的列不会有水,因为它大于了两边较矮的墙。
较矮的墙的高度等于当前列的墙的高度。
和上一种情况是一样的,不会有水。
明白了这三种情况,程序就很好写了,遍历每一列,然后分别求出这一列两边最高的墙。找出较矮的一端,和当前列的高度比较,结果就是上边的三种情况。
总结
动态规划解法通过预处理左右最高墙,高效计算每个位置的积水量。该方法思路清晰,时间复杂度低,是解决此类问题的经典方法。