这是 代码随想录 的学习记录帖
首先尝试进行暴力解法,利用双指针进行求解,每次寻找 i 左右最高点,那么 i 所能接的雨水便是 min(LHeight, rHeight) - height[i],但这样暴力求解,无法通过力扣的最后一个测试用例,超时了。
class Solution {
public:
int trap(vector<int>& height) {
int sum = 0;
for(int i = 0; i < height.size(); i++){
if(i == 0 || i == height.size() - 1){
continue;
}
int leftHight = height[i];
int rightHight = height[i];
for(int l = i - 1; l >= 0; l--){
leftHight = max(leftHight, height[l]);
}
for(int r = i + 1; r < height.size(); r++){
rightHight = max(rightHight, height[r]);
}
if(min(leftHight, rightHight) - height[i] > 0){
sum += min(leftHight, rightHight) - height[i];
}
}
return sum;
}
};
利用双指针进行求解,统计的是每一列所能接的雨水,在每个 i 处都遍历 i 的左右最高点,是进行了许多重复的操作的,可以使用动规的思想,记录已经遍历过的点的左右最高点。这样就能减少遍历的重复操作。
class Solution {
public:
int trap(vector<int>& height) {
if(height.size() <= 2){
return 0;
}
int sum = 0;
vector<int> leftmax(height.size(), 0);
vector<int> rightmax(height.size(), 0);
leftmax[0] = height[0];
rightmax[height.size() - 1] = height[height.size() - 1];
for(int i = 1; i < height.size(); i++){
leftmax[i] = max(leftmax[i - 1], height[i]);
}
for(int j = height.size() - 2; j >= 0; j--){
rightmax[j] = max(rightmax[j + 1], height[j]);
}
for(int i = 1; i < height.size() - 1; i++){
if(min(leftmax[i], rightmax[i]) - height[i] > 0){
sum += min(leftmax[i], rightmax[i]) - height[i];
}
}
return sum;
}
};
力扣的官方题解提供了一种空间复杂度更低的双指针法,对于位置 i 的接水量取决于 leftMax 和 rightMax 中的较小者,所以我们不必真的知道较大者是谁,只要知道较小者是谁就可以了。
初始化 left = 0, right = n-1, leftMax = 0, rightMax = 0
注意到对于位置 left 来说, leftMax 是真正意义上的左侧最大值, 而 rightMax 不是真的右侧最大值
而对于位置 right 来说, rightMax 是真正意义上的右侧最大值, 而 leftMax 不是真的左侧最大值
对于 height[left] < height[right],则必有 leftMax < rightMax;如果 height[left] ≥ height[right],则必有 leftMax ≥ rightMax这句话的理解:
如果当前轮的双指针为 left 和 right,那么上一轮要么是 left - 1 和 right,要么是 left 和 right + 1。
如果移动的是 left 指针,即 left - 1 -> left,那么 height[0, left- 1] < height[right] 且 height[right] = rightMax
如果移动的是 right 指针,即 right <- right + 1,那么 height[left] >= height[right + 1, n - 1] 且 height[left] = leftMax
现在,我们再回看官方的描述: 如果 height[left] < height[right],则必有 leftMax < rightMax
假设上一轮移动的是 left 指针,有 height[0, left- 1] < height[right],又 height[left] < height[right],则 height[0, left] < height[right] = rightMax
假设上一轮移动的是 right 指针,有 height[left] >= height[right + 1, n - 1],又 height[left] < height[right],则 leftMax = height[left] < height[right]
题解评论中有一个“打擂台”的评论,是很形象的
class Solution {
public:
int trap(vector<int>& height) {
int ans = 0;
int left = 0, right = height.size() - 1;
int leftMax = 0, rightMax = 0;
while (left < right) {
leftMax = max(leftMax, height[left]);
rightMax = max(rightMax, height[right]);
if (height[left] < height[right]) {
ans += leftMax - height[left];
++left;
} else {
ans += rightMax - height[right];
--right;
}
}
return ans;
}
};
接下来是使用单调栈的解法,上述都是双指针的解法,是对每一列来计算能接到的雨水,之后将其相加。而单调栈的解法,则是横向的来计算。
从栈头(元素从栈头弹出)到栈底的顺序应该是从小到大的顺序。一旦发现添加的柱子高度大于栈头元素了,此时就出现凹槽了,栈头元素就是凹槽底部的柱子,栈头第二个元素就是凹槽左边的柱子,而添加的元素就是凹槽右边的柱子。遇到相同的元素,更新栈内下标,就是将栈里元素(旧下标)弹出,将新元素(新下标)加入栈中。
情况一:当前遍历的元素(柱子)高度小于栈顶元素的高度 height[i] < height[st.top()]
情况二:当前遍历的元素(柱子)高度等于栈顶元素的高度 height[i] == height[st.top()]
情况三:当前遍历的元素(柱子)高度大于栈顶元素的高度 height[i] > height[st.top()]
class Solution {
public:
int trap(vector<int>& height) {
stack<int> st;
int ans = 0;
st.push(0);
for(int i = 1; i < height.size(); i++){
if(height[i] < height[st.top()]){
st.push(i);
}else if(height[i] == height[st.top()]){
st.pop();
st.push(i);
}else{
while(!st.empty() && height[i] > height[st.top()]){
int mid = st.top();
st.pop();
if(!st.empty()){
int w = i - st.top() - 1;
int h = min(height[st.top()], height[i]) - height[mid];
ans += w * h;
}
}
st.push(i);
}
}
return ans;
}
};
精简后的代码:将情况1 情况2合并处理,此时栈内可能会存在多个相等的元素,与精简前只用处理一次不同,这里要处理多次,但如果重复项有n,那么前n-1次处理,h 都为 0;
class Solution {
public:
int trap(vector<int>& height) {
stack<int> st;
st.push(0);
int sum = 0;
for (int i = 1; i < height.size(); i++) {
while (!st.empty() && height[i] > height[st.top()]) {
int mid = st.top();
st.pop();
if (!st.empty()) {
int h = min(height[st.top()], height[i]) - height[mid];
int w = i - st.top() - 1;
sum += h * w;
}
}
st.push(i);
}
return sum;
}
};