写在前面:
昨天和美国的小伙伴们聚会吃饭打游戏到两点,今天早上 9 点起感觉自己如修仙了一般,拖着沉重的身躯面对 Trapping Rain Water 真是太折磨了。广大码友还是要尽量早睡早起才能有充足的脑子写代码肝算法o~~~
今天的题目是 Trapping Rain Water,应该是 Leetcode 序列最靠前的一道 Hard 题目之一了,也是究极元老,在经历了 Leetcode 2000 多道题目以后依然经久不衰成为很多人刷题路上最难啃的骨头之一,题主也是写了3 - 4 次在最终理解他的算法后面的 mindset。作为一道 Dynamic Programming 和 一维 array 的基础上手题(对于理解 DP 很有帮助,但是对于脑子很有挑战🤣),今天我们就来一起 work through 一下这道题,看看能否与各位小伙伴们一起跨过这道最古老的 Leetcode Hard!
题目介绍:
题目信息:
- 题目地址:https://leetcode.com/problems/trapping-rain-water/description/
- 题目类型:Array, Dynamic Programming
- 题目难度:Hard (It's deadly tricky in the mindset and surprisingly easy in implementation)
- 题目来源:Google 面试高频真题
题目问题:
- 给定一个由数组表示的多个立方体组合图形,计算出他最多能储存多少单位的雨水
- 例子:
- 给定一个数组,每一个数组表示在当前横轴位置的高度:
[0,1,0,2,1,0,1,3,2,1,2,1]
- 他表示的图形为:
- 这其中,蓝色的部分代表能储存水的大小,如图所示,这个例子中可以储存 6个单位
- 这个例子中的答案则为 maxStoring = 6
- 给定一个数组,每一个数组表示在当前横轴位置的高度:
题目想法:
如何计算出雨水和高度的关系:
单一拿出一个二纬平面我们来看,在单一位置的储水量取决于他相邻左右两个柱子中更短的那一个。
假设我们是在 [1, 0, 3] 的例子中,这里的最大储水量可以表示为:min(leftMax, rightMax) = min(1, 3) = 1, 在 0 这个位置我们可以储存一格水。我们在看 1 和 3 的位置,这两个位置从现实角度来看是不能储水的,因为他们左右没有柱子能将他们合围起来,水会从这里流出去。Ok,基本规律找到,那我们怎么样能用算法把他们关联起来呢?
从 0 的位置我们可以看出,之所以能有一格储水量,是因为 min(leftMax, rightMax) 和 他本身的 height = 0 之间还有 1格的空隙,所以储水量表示为:min(leftMax, rightMax) - height = 0。
我们将这个关系推广一下试试,假设 1 和 3 两边还有柱子:
[2, 1, 0, 1, 3]
对应介绍例子图中的这一块:
边缘的 2 和 3 因为是 left-end 和 right-end 所以没有储存水的可能,对于 2nd,3rd,和 4th 格子的地方,是否对应我们找到的关系呢?
2nd:储水量为:min(leftMax[2nd], rightMax[2nd]) - height[2nd] = min(2, 3) - 1 = 1 符合实际
3rd: 储水量为:min(leftMax[3rd], rightMax[3rd]) - height[3rd] = min(2, 3) - 0 = 2 符合实际
4th: 储水量为:min(leftMax[4th], rightMax[4th]) - height[4th] = min(2, 3) - 1 = 1. 符合实际
总区间储水量为:1 + 2 + 1 = 4,也符合实际
由此我们可以总结出:对应特定位置,当前位置的最大可储水量为:
min(leftMax[i], rightMax[i]) - height[i]
而总的可储水量答案为: (Min(leftMax[i], rightMax[i]) - height[i])
Dynamic Programming 优化:
我们可以注意到在上面的公式的关系中,一直会有 leftMax[i] 和 rightMax[i] 的信息介入。而我们在一个特定位置找到在这个位置向左的最大值和向右的最大值,则加起来需要做一次数组遍历,这样会让我们的算法在时间复杂度上变成 O(N^2)
但是因为这个 leftMax 和 rightMax 的所有值都是 static 的,不会随着我们的遍历而改变,这就给了我们一个用 Dynamic Programming 来存储的优化空间。即我们可以在开始时先从左到右遍历,找出每个位置的 leftMax,再用相同的办法从右到左找出每一个未知的 rightMax。在后续的公式计算时,可以直接调取对应位置上的 leftMax 和 rightMax 值就可
时间复杂度上就变为 O(n) + O(2n) = O(3n) = O(N)!
题目解法:
- 定义两个长度为 N(题目给定 heights 数组长度) 的数组,分别记录 leftMax,rightMax
- 定义 ans = 0
- leftMax[0] 设为 height[0],rightMax[-1] 设为 height[-1] (-1 代表最后一个元素,per python)
- 从左第二个到右遍历数组:
- leftMax[i] = max(height[i], leftMax[i-1])
- 从右倒数第二个到左遍历数组:
- rightMax[i] = max(height[i], rightMax[i+1])
- 遍历所有数组:
- ans[i] = min(leftMax[i], rightMax[i]) - height[i])
- ans += ans[i]
题目代码:
class Solution {
public:
int trap(vector<int>& heights) {
//define as the highest value from i to left-side-end
vector<int> leftMax(heights.size());
//define as the highest value from i to right-side-end
vector<int> rightMax(heights.size());
int ans = 0;
leftMax[0] = heights[0];
rightMax[heights.size()-1] = heights[heights.size()-1];
//traverse from left to right to find the leftMax
for(int i = 1; i < heights.size(); i++){
leftMax[i] = max(heights[i], leftMax[i-1]);
}
//traverse from right to left to find the rightMax
for(int i = heights.size()-2; i >= 0; i--){
rightMax[i] = max(heights[i], rightMax[i+1]);
}
//traverse each direction and add the min(leftMax, rightMax) - height[i]
for(int i = 0; i < heights.size(); i++){
ans += min(leftMax[i], rightMax[i]) - heights[i];
}
return ans;
}
};
- 时间复杂度 O(N)
- 空间复杂度 O(N)