题目描述
给定 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
题解思路
方法一:暴力
对于数组中的每个元素,我们找出下雨后水能达到的最高位置,等于两边最大高度的较小值减去当前高度的值。
- 初始化 ans=0
- 从左向右扫描数组:
初始化 max_left=0 和 max_right=0- 从当前元素向左扫描并更新:
max_left=max(max_left,height[j]) - 从当前元素向右扫描并更新:
max_right=max(max_right,height[j]) - 将 min(max_left,max_right)−height[i] 累加到 res
- 从当前元素向左扫描并更新:
复杂性分析
- 时间复杂度: O(n^2)。数组中的每个元素都需要向左向右扫描。
- 空间复杂度 O(1) 的额外空间。
代码实现:
class Solution {
public:
int trap(vector<int>& height) {
int res = 0, sz = height.size();
for (int i = 1; i < sz - 1; ++i) {
int max_left = 0, max_right = 0;
for (int j = i; j >= 0; --j) {
max_left = max(height[j], max_left);
}
for (int j = i; j < sz; ++j) {
max_right = max(height[j], max_right);
}
res += min(max_left, max_right) - height[i];
}
return res;
}
};
方法二:动态编程(方法一的优化)
- 找到数组中从下标 i 到最左端最高的条形块高度 left_max。
- 找到数组中从下标 i 到最右端最高的条形块高度 right_max。
- 扫描数组 height 并更新答案:
累加 min(max_left[i],max_right[i])−height[i] 到 res 上
复杂性分析
- 时间复杂度:O(n)。
存储最大高度数组,需要两次遍历,每次 O(n) 。
最终使用存储的数据更新 res ,O(n)。 - 空间复杂度:O(n) 额外空间。
和方法 1 相比使用了额外的 O(n) 空间用来放置 left_max 和 right_max 数组。
代码实现:
class Solution {
public:
int trap(vector<int>& height) {
int res = 0, sz = height.size();
if (sz == 0) return 0;
vector<int> max_left(sz), max_right(sz);
max_left[0] = height[0];
for (int i = 1; i < sz ; ++i) {
max_left[i] = max(height[i], max_left[i-1]);
}
max_right[sz-1] = height[sz-1];
for (int i = sz - 2; i >= 0; --i) {
max_right[i] = max(height[i], max_right[i+1]);
}
for (int i = 1; i < sz - 1; ++i) {
res += min(max_left[i], max_right[i]) - height[i];
}
return res;
}
};
方法三:栈
在遍历数组时维护一个栈。如果当前的条形块小于或等于栈顶的条形块,我们将条形块的索引入栈,意思是当前的条形块被栈中的前一个条形块界定。如果我们发现一个条形块长于栈顶,我们可以确定栈顶的条形块被当前条形块和栈的前一个条形块界定,因此我们可以弹出栈顶元素并且累加答案到 res 。
- 使用栈来存储条形块的索引下标。
- 遍历数组:
- 当栈非空且 height[cur]>height[st.top()]
- 意味着栈中元素可以被弹出。弹出栈顶元素 top。
- 计算当前元素和栈顶元素的距离,准备进行填充操作
distance=cur−st.top()−1 - 找出界定高度
bounded_height=min(height[current],height[st.top()])−height[top] - 往答案中累加积水量 res+=distance×bounded_height
- 将当前索引下标入栈
- 将 cur 移动到下个位置
- 当栈非空且 height[cur]>height[st.top()]
复杂性分析: aa
- 时间复杂度:O(n)。
单次遍历 O(n) ,每个条形块最多访问两次(由于栈的弹入和弹出),并且弹入和弹出栈都是 O(1) 的。 - 空间复杂度:O(n)。 栈最多在阶梯型或平坦型条形块结构中占用 O(n) 的空间。
代码实现:
class Solution {
public:
int trap(vector<int>& height) {
int res = 0, cur = 0;
stack<int> st;
while (cur < height.size()) {
while (!st.empty() && height[cur] > height[st.top()]) {
int top = st.top();
st.pop();
if (st.empty()) break;
int distance = cur - st.top() - 1;
int bounded_height = min(height[cur], height[st.top()]) - height[top];
res += distance * bounded_height;
}
st.push(cur++);
}
return res;
}
};
方法四:双指针法
从动态编程方法的示意图中我们注意到,只要 right_max[i]>left_max[i] (元素 0 到元素 6),积水高度将由 left_max 决定,类似地 left_max[i]>right_max[i](元素 8 到元素 11)。
所以我们可以认为如果一端有更高的条形块(例如右端),积水的高度依赖于当前方向的高度(从左到右)。当我们发现另一侧(右侧)的条形块高度不是最高的,我们则开始从相反的方向遍历(从右到左)。
我们必须在遍历时维护 left_max 和 right_max ,但是我们现在可以使用两个指针交替进行,实现 1 次遍历即可完成。
复杂性分析:
- 时间复杂度:O(n)。单次遍历的时间 O(n)。
- 空间复杂度:O(1) 的额外空间。left, right, left_max 和 right_max 只需要常数的空间。
代码实现:
class Solution {
public:
int trap(vector<int>& height) {
int left = 0, right = height.size() - 1;
int ans = 0;
int left_max = 0, right_max = 0;
while (left < right) {
if (height[left] < height[right]) {
height[left] >= left_max ? (left_max = height[left]) : ans += (left_max - height[left]);
++left;
}
else {
height[right] >= right_max ? (right_max = height[right]) : ans += (right_max - height[right]);
--right;
}
}
return ans;
}
};