Bootstrap

接雨水★-力扣

这是 代码随想录 的学习记录帖
首先尝试进行暴力解法,利用双指针进行求解,每次寻找 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;
    }
};

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;