42. 接雨水
题目描述
给定 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
提示:
- n = = h e i g h t . l e n g t h n == height.length n==height.length
- 0 ≤ n ≤ 3 ∗ 1 0 4 0 \le n \le 3 * 10^4 0≤n≤3∗104
- 0 ≤ h e i g h t [ i ] ≤ 105 0 \le height[i] \le 105 0≤height[i]≤105
题解:
这题可以从两个方向考虑,一种是竖着,一种是横着。
法一(竖着):
只考虑 i
位置上方能放几格水。那么很容易想到 i
位置上方水的数量的表达式:max{min{ i 左侧的最大值, i 右侧的最大值} - height[i], 0}
。
对所有位置的结果累加求和就是最终结果。然后再求上述表达式的结果时,就有一些值得探究的地方了。
-
最基础的解法就是暴力了,对于每个位置
i
,用两个循环求[0...i-1]
的最大值和[i+1,n-1]
的最大值。但是这题数据肯定过不去,代码就不贴了。 -
其实可以不用那么暴力,我们可以额外使用两个数组
leftMax
和rightMax
,leftMax[i]
表示[0...i]
的最大值,right[i]
表示[i...n-1]
的最大值。从左往右遍历一遍,生成leftMax
;从右往左一遍,生成rightMax
。代码如下:class Solution { public: int trap(vector<int>& height) { int n = height.size(); if ( n < 3 ) return 0; vector<int> leftMax( n ), rightMax( n ); leftMax[0] = height[0]; for ( int i = 1; i < n; ++i ) leftMax[i] = max( leftMax[i - 1], height[i] ); rightMax[n - 1] = height[n - 1]; for ( int i = n - 2; i >= 0; --i ) rightMax[i] = max( rightMax[i + 1], height[i] ); int ret = 0; for ( int i = 1; i < n - 1; ++i ) ret += max( min( leftMax[i - 1],rightMax[i + 1] ) - height[i], 0 ); return ret; } }; /* 时间:8ms,击败:90.86% 内存:14.1MB,击败:90.60% */
-
其实还可以使用双指针,做到额外空间复杂度 O ( 1 ) O(1) O(1) 。具体就是:使用两个指针
l
和r
,开始分别指向0
和n - 1
,两个变量leftMax
和rightMax
,分别表示l
左边的最大值和r
右边的最大值,开始时为arr[0]
和arr[n - 1]
,每一步让l
移动或者r
移动,具体分情况讨论:- 若
leftMax <= rightMax
,此时可以求出l
位置上方的水量。因为rightMax
是arr[r + 1...n - 1]
的最大值,而l
的右侧还有一个未知区域,所以l
右侧最大值一定不会小于rightMax
。leftMax
代表l
左侧的最大值,因为leftMax <= rightMax
,可知leftMax
是l
位置的瓶颈。故l
位置上方的水量为max{leftMax - arr[l], 0}
,然后让l
向右移动,并且移动前需要更新leftMax = max{leftMax, arr[l]}
。 - 若
leftMax > rightMax
,分析同 1。
代码如下:
class Solution { public: int trap(vector<int>& height) { int n = height.size(); if ( n < 3 ) return 0; int lmax = height[0], rmax = height[n - 1]; int ret = 0, l = 1, r = n - 2; while ( l <= r ) { if ( lmax <= rmax ) { ret += max( lmax - height[l], 0 ); lmax = max( lmax, height[l++] ); } else { ret += max( rmax - height[r], 0 ); rmax = max( rmax, height[r--] ); } } return ret; } }; /* 时间:0ms,击败:100.00% 内存:13.8MB,击败:95.18% */
- 若
法二(横着):
就是考虑一个空格往左往右最长能延长到什么地方(按层的意思)。
我们可以观察到,当柱子之间形成一个 u
形槽时,就会积累雨水。我们维护一个栈底到栈顶严格递减的单调栈,如果 arr[i] >= arr[stk.top()]
,说明此时形成了 凹槽 ,将栈顶元素弹出,记为 top
,此时增加的雨水量是:(i - stk.top() - 1) * (min(arr[i], arr[stk.top()]) - arr[top])
。
法二代码:
class Solution {
public:
int trap(vector<int>& height) {
int n = height.size();
if ( n < 3 ) return 0;
vector<int> stk;
int top, ret = 0;
for ( int i = 0; i < height.size(); ++i ) {
while ( stk.size() && height[stk.back()] <= height[i] ) {
top = stk.back();
stk.pop_back();
if ( !stk.size() ) break;
ret += ( i - stk.back() - 1 ) * (min( height[i], height[stk.back()] ) - height[top]);
}
stk.push_back( i );
}
return ret;
}
};
/*
时间:8ms,击败:90.86%
内存:13.9MB,击败:94.12%
*/