[单调栈 ]42.接雨水 84.柱状图中最大的矩形(暴力法 -> 单调栈)

42.接雨水

题目链接:42.接雨水

分类:数组、栈(单调栈)、动态规划、双指针

在这里插入图片描述
这类题目就是遍历数组,把遍历多次才能解决的问题用辅助空间优化为只需要遍历一次就能求解。也就是先找到求解的暴力解法,然后对暴力解法做优化,所有优化措施都可以从暴力解法出发推导出来,暴力解法也能更好地帮助我们理解优化解法,所有我认为在做这类题时,暴力解法实际上才是整个问题的关键。

思路1:暴力解(所有解法的基本思想,很关键)

对于每个元素nums[i],都向它的左右两侧遍历寻找元素值 > nums[i]且是最大的两个左右边界,

每个下标i处围成的雨水单位 = min{最大左边界,最大右边界} - nums[i],

最后将这些雨水单位叠加即可。

实现代码

class Solution {
    public int trap(int[] height) {
        int len = height.length;
        int total = 0;
        for(int i = 0; i < len; i++){
            //寻找左边界:对于height[i]向左寻找>height[i]的最大边界
            //int left = i - 1;
            int maxLeft = height[i];
            for(int left = i - 1; left >= 0; left--){
                if(height[left] > maxLeft) maxLeft = height[left];
            }
            if(maxLeft == height[i]) continue;//没有找到最大左边界,直接跳过当前height[i]

            //寻找右边界:对于height[i]向右寻找>height[i]的最大边界
            //int right = i + 1;
            int maxRight = height[i];
            for(int right = i + 1; right < len; right++){
                if(height[right] > maxRight) maxRight = height[right];
            }
            if(maxRight == height[i]) continue;//没有找到最大右边界,直接跳过当前height[i]
            //找到height[i]的左右边界,计算围成的雨水单位
            int rain = Math.min(maxLeft, maxRight) - height[i];
            total += rain;
        }
        return total;
    }
}

思路2:动态规划

基于思路1暴力解的优化,用两个辅助数组left、right把每个元素的最大左右边界保存起来,不必每次都遍历到首尾来找左右边界。

  • 状态设置:left[i]表示num[i]的最大左边界,right[i]表示nums[i]的最大右边界。
  • 状态转移
    left[i]的计算是基于left[i-1]而来的,right[i]的计算是基于right[i+1]而来。
    所以构造left,right需要分别从前往后和从后往前遍历数组,构造left,right。
left[i] = Math.max(left[i - 1], height[i - 1]);
right[i] = Math.max(right[i + 1], height[i + 1]);

我在实现时把两个遍历过程合并为一次遍历,right数组的下标需要一定的转换,可以先写出两次遍历代码,再合并为一次遍历,不容易混乱。
合并后代码如下:

 //同时构造left,right数组
 for(int i = 0; i < len; i++){
     if(i > 0){
         left[i] = Math.max(left[i - 1], height[i - 1]);
     }
     if(i < len - 1 && len - i - 1 < len - 1){
         right[len - i - 1] = Math.max(right[len - i - 1 + 1], height[len - i - 1 + 1]);
     }
 }

实现代码:

class Solution {
    public int trap(int[] height) {
        int len = height.length;
        int total = 0;
        int[] left = new int[len];
        int[] right = new int[len];
        //同时构造left,right数组
        for(int i = 0; i < len; i++){
            if(i > 0){
                left[i] = Math.max(left[i - 1], height[i - 1]);
            }
            if(i < len - 1 && len - i - 1 < len - 1){
                right[len - i - 1] = Math.max(right[len - i - 1 + 1], height[len - i - 1 + 1]);
            }
        }
        //遍历height数组,根据预先找到的最大左右边界按列计算雨水单位
        for(int i = 0; i < len; i++){
            //如果最大边界小于等于第i个元素,则直接跳过
            if(left[i] <= height[i] || right[i] <= height[i]) continue;
            //如果最大边界>第i个元素,则取两个边界中的较小值计算第i个下标的雨水单位
            else{
                int rain = Math.min(left[i], right[i]) - height[i];
                total += rain;
            }
        }

        return total;
    }
}

思路3:双指针法(动态规划的空间优化,最佳解法)

参考:https://leetcode-cn.com/problems/trapping-rain-water/solution/jie-yu-shui-by-leetcode/ 的评论。

基于思路2动态规划的状态转移方程可以发现,计算left[i]时只需要知道left[i - 1],计算right[j] 只需要知道right[j + 1],所以可以针对这一点做空间上的优化,用两个变量代替两个辅助数组,同时在变量状态转移的过程中就计算接到的雨水单位。

实现代码:

    public int trap(int[] height) {
        int len = height.length;
        if(len < 3) return 0;
        int total = 0;
        int left = 0, right = len - 1;
        int leftMax = 0, rightMax = 0;

        while(left <= right){
            //只需要左边界最大小于当前临时的最大右边界就能计算出第i处的雨水单位
            if(leftMax <= rightMax){
                total += Math.max(0, leftMax - height[left]);
                leftMax = Math.max(leftMax, height[left]);
                left++;
            }
            else{
                total += Math.max(0, rightMax - height[right]);
                rightMax = Math.max(rightMax, height[right]);
                right--;
            }
        }
        return total;
    }

思路4:单调栈(按行计算 + 算法原理改进,推荐解法)

分析

1、要想一次遍历就计算出总的雨水单位,必定要在遍历数组每一列的同时计算出这一列能接到的雨水单位,叠加到total上。
2、举例 或 由思路3的双指针法可以发现,计算第i列的雨水单位,并不需要分别找出左右的最大边界,只需要找到大于nums[i]的左右边界,且当找到的左边界<右边界,就可以计算出第i列的雨水单位 = nums[左边界] - nums[i];(改进暴力解的算法原理)

如何用栈来实现这一过程?

从头开始遍历数组:

  • 如果nums[i]<栈顶,说明当前这一列的高度小于栈顶的高度,能够接雨水,所以nums[i]入栈,此时栈里nums[i]的前一个元素就是大于nums[i]的一个左边界,只需要再判断接下来的右边界是否>nums[i]即可;
  • 如果nums[i]>栈顶,说明当前这一列的高度大于栈顶的高度,当前这一列就可以作为栈顶的右边界,弹出栈顶得到nums[i-1],顶替上来的栈顶就是nums[i-1]的左边界,则雨水单位=Math.min(nums[i],顶替上来的栈顶) - nums[i-1],
  • 如果nums[i]==栈顶,可以发现操作步骤和<相同。

栈的思想和我们人脑直观上解这题的步骤一样,遇到右边界,就回去找左边界,左右边界之间的就是能够接到的雨水单位;但如果每次都回去找左边界,则时间复杂度很高,所以把左边界备份起来。同时使用栈来使左右边界能够配对,什么是配对?
如图,[2,3]是配对的,[2,3]之间的[1,1]又是配对的,所以栈的实质是横向按行来计算雨水单位。

实现代码:

class Solution {
    public int trap(int[] height) {
        int len = height.length;
        if(len < 3) return 0;
        int total = 0;
        int left = 0, right = len - 1;
        Stack<Integer> stack = new Stack<>();//存放元素的下标
        while(left < len){
            //栈为空,或 height[i]<=栈顶
            while(!stack.isEmpty() && height[left] > height[stack.peek()]){
                int top = stack.peek();
                stack.pop();
                //栈里如果弹出栈顶后为空,则直接退出
                if(stack.isEmpty()) break;
                int distance = left - stack.peek() - 1;
                int lessIndex = height[stack.peek()] < height[left] ? stack.peek() : left;
                int temp = height[lessIndex] - height[top];
                total += distance * temp;
            }
            stack.push(left);
            left++;
        }        
        return total;
    }
}

84. 柱状图中最大的矩形

题目链接:84. 柱状图中最大的矩形

分类:数组、栈(单调栈)

在这里插入图片描述

题目分析

这题和42.接雨水很相似,暴力解法的思路也很类似,只要找到暴力解的思路,后续的解法就是对暴力解的优化。

思路1:暴力解

对每个高度都计算以当前高度为高的最大矩形面积,以第i个高度为高,向该柱子的左右两边遍历寻找 >= 该高度的最远左右边界,一旦遇到 < 当前柱子高度的柱子,就停止寻找,或直到高度数组边界。

以该柱子为高的最大矩形面积 = 柱高*左右边界距离。

例如
[2,1,5,6,2,3],
以2为高,向左已到达数组边界,向右遇到1 < 2,停止寻找,所以以第0个柱子为高的最大矩形面积 = 2 * 1=2;
以1为高,向左可以遍历到数组边界0,向右也可以遍历到数组边界 len-1,所以以第1个柱子为高的最大矩形面积 = 1 * (len-1-0+1)=6
以此类推。

维护一个max保存计算过程中得到的最大矩形面积,在所有柱子都计算完毕后最后返回max即可。

存在的问题:效率低,做了很多重复计算

实现代码:

class Solution {
    public int largestRectangleArea(int[] heights) {
        if(heights == null || heights.length == 0) return 0;
        int max = 0;
        for(int i = 0; i < heights.length; i++){
            //向左寻找最远的>=heights[i]的元素
            int left = i;
            while(left >= 0 && heights[left] >= heights[i]){
                left--;
            }
            left++;//退出循环后+1才是实际找到的左边界元素下标
            int right = i;
            while(right < heights.length && heights[right] >= heights[i]){
                right++;
            }
            right--;//退出循环后-1才是实际找到的右边界元素下标
            max = Math.max(max, (right - left + 1) * heights[i]);
        }
        return max;
    }
}
另一个思路:动态规划(没找到可行方法)

和42题一样也考虑动态规划,用两个数组left,right保存左右边界,避免重复计算。
但可以发现,第i个柱子希望找到的左右边界是>=第i个柱子高度且最远的元素,最
终用来计算矩形面积需要用到元素下标,但寻找满足条件的边界元素是要根据元素值。
而且寻找左边界left[i]也不能仅根据left[i-1]而来,所以不适合用动态规划,
至少不适合用类似第42题的动态规划。

思路2:单调栈 + 哨兵技巧

和42题单调栈解法的思路相同,只是出入栈的规则做一点修改。简单来说就是栈里存放的是栈顶柱子的左边界,栈外要入栈的元素可能是栈顶柱子的右边界。而且栈存放的都是下标。

维护一个栈,为了能够计算第一个入栈的柱子的最大矩形面积,初始时在栈底加入-1(哨兵),-1也可以用作判断算法是否结束的标志。
取heights数组的第i个元素:

  • 如果栈只有一个栈底元素-1时,元素下标入栈;
  • 如果栈不止一个栈底元素时,取栈顶下标对应的元素值和heights[i]比较:
    • 如果heights[i]>=heights[栈顶],则元素下标i入栈;
    • 如果heights[i]< heights[栈顶],则可以计算以heights[栈顶]为高的矩形最大面积,其中元素i下标作为矩形的右边界,栈顶的下一个元素作为矩形的左边界,最大矩形面积 = (右边界下标 - 左边界下标-1) * 高=(i下标 - 栈顶下一个元素的下标 - 1) * 栈顶对应的高。计算这个面积后,要及时将栈顶弹出,同时继续比较新的栈顶和heights[i]的大小关系,直到heights[i] >= heights[栈顶]时才退出,所以这里要使用一个while循环不断弹出栈顶,而且当退出循环时,如果i < heights.length(避免第二个哨兵-1也入栈),要记得将 i 压入栈中(这三点极易忽略:1、栈顶弹出 2、循环判断栈顶 3、待入栈元素下标i入栈)
这里的单调栈思想如果不理解可以回看42题的单调栈解法分析。

简单来说:

右边界 - 左边界 - 1;
右边界=待入栈元素的下标;
左边界=栈顶的下一个元素的下标;

如果到达heights末尾时,栈还不止一个栈底元素-1,则构造一个下标=heights.length,值 = -1的元素作为“哨兵”和栈内剩下的元素做比较,重复上面的步骤,以便将栈内所有剩余元素都出栈(除第一个哨兵外)。直到栈里只剩一个-1,且待入栈元素值也为-1,即两哨兵相遇,则算法结束。

例如

[2,1,5,6,2,3],初始时向栈底压入-1;
nums[0]=2,因为栈内只有一个栈底元素,所以下标0直接入栈。栈=[-1,0]
nums[1]=1<nums[栈顶=0]=2,所以nums[1]的下标1就作为以当前栈顶为高的矩形的右边界,栈顶的下一个元素下标-1就是这个矩形的左边界,所以以栈顶nums[0]=2为高的最大矩形面积=(右边界-左边界)*高=(1-(-1)-1)*2=2;接着,弹出栈顶,因为此时栈里只有一个初始的栈底元素-1,所以nums[1]直接入,此时栈=[-1,1]
nums[2]=5>nums[栈顶=1]=1,所以下标2入栈,栈=[-1,1,2]
nums[3]=6>nums[栈顶=2]=5,所以下标3入栈,栈=[-1,1,2,3]
nums[4]=2<nums[栈顶=3]=6,所以nums[4]的下标4就作为以当前栈顶为高的矩形的右边界,栈顶的下一个元素下标2就是这个矩形的左边界,所以以栈顶nums[3]=6为高的最大矩形面积=(右边界-左边界)*高=(4-2-1)*6=6;接着,弹出栈顶,因为此时栈=[-1,1,2],nums[4]=2<栈顶nums[2]=5,所以还可以继续计算以nums[2]为高的矩形面积(这里记得要用while循环不断弹出>heights[i]的栈顶计算对应的矩形面积):
nums[4]=2<nums[栈顶=2]=5,所以nums[4]的下标4就作为以当前栈顶为高的矩形的右边界,栈顶的下一个元素下标1就是这个矩形的左边界,所以以栈顶nums[2]=5为高的最大矩形面积=(4-1-1)*5=10,;接着,弹出栈顶,此时栈=[-1,1],nums[4]=2>nums[栈顶=1]=1,所以下标4入栈,得栈=[-1,1,4]
nums[5]=3>nums[栈顶=4]=2,所以下标5入栈,栈=[-1,1,4,5]。
到达heights数组末尾,所以设接下来的入栈元素下标=6,元素值=-1.
nums[6]=-1<nums[栈顶=5]=3,所以nums[6]的下标6作为以当前栈顶为高的矩形的右边界,栈顶的下一个元素下标4是左边界,所以最大矩形面积=(6-4-1)*3=3;弹出栈顶,此时栈=[-1,1,4],继续拿下标=6,元素值=0作为入栈元素;
nums[6]=-1<nums[栈顶=4]=2,所以nums[6]的下标6作为当前栈顶为高的矩形的右边界,栈顶的下一个元素下标1是左边界,所以最大矩形面积=(6-1-1)*2=8;弹出栈顶,此时栈=[-1,1],继续拿下标=6,元素值=0作为入栈元素。
nums[6]=-1<nums[栈顶=1]=1,所以ums[6]的下标6作为当前栈顶为高的矩形的右边界,栈顶的下一个元素下标-1是左边界,所以最大矩形面积=(6-(-1)-1)*1=6;弹出栈顶,此时栈=[-1],当栈内只剩-1时,算法结束。

其中,思路3里在栈底预先加入的-1,和到达heights数组末尾时构造的-1元素,就是所谓的“哨兵”,
有了这两个哨兵:

  • 预先加入的-1:由于它一定比输入数组里任何一个元素小,它肯定不会出栈,因此栈一定不会为空;
  • 最后构造的-1:正因为它一定比输入数组里任何一个元素小,它会让所有输入数组里的元素出栈(第 1 个哨兵元素除外)。

实现代码(更好理解)

class Solution {
    public int largestRectangleArea(int[] heights) {
        if(heights == null || heights.length == 0) return 0;
        int max = 0;
        Stack<Integer> stack = new Stack<>();
        stack.push(-1);
        for(int i = 0; i <= heights.length; i++){
            if(i == heights.length){
                //数组所有元素都已入过栈且栈只剩-1时
                if(stack.size() == 1) break;
                //数组所有元素都已入过栈且栈剩余元素不止-1时
                else{
                    //构造下标=heights.length,值为-1的元素
                    int value = -1;
                    while(stack.size() > 1){
                        int right = i;
                        int high = heights[stack.pop()];
                        int left = stack.peek();
                        max = Math.max(max, (right - left - 1) * high);
                    }

                }
            } 
            //数组还有未入栈元素时(i还未到达数组末尾)
            else{
                //栈内只有一个-1时,则元素入栈
                if(stack.size() == 1) stack.push(i);
                //栈内不止一个-1时,取栈顶与元素i比较
                else{
                    int top = stack.peek();
                    if(heights[i] >= heights[top]) stack.push(i);
                    else{
                        //易错点:遇到heights[i]<heights[栈顶]时,需要不断弹出栈顶计算对应的矩形面积,直到栈底只剩-1或栈顶元素>=heights[i]
                        while(stack.size() > 1 && heights[i] < heights[stack.peek()]){
                            int right = i;//元素i下标作为右边界
                            int high = heights[stack.pop()];//弹出栈顶,取出对应的高
                            int left = stack.peek();//顶替上来的下标作为左边界
                            max = Math.max(max, (right - left - 1) * high);
                        }
                        //将当前元素下标入栈(易忽略!!!)
                        stack.push(i);
                    }
                }
            }
        }
        return max;
    }
}

代码简化(推荐版本2)

变得不那么好理解,是基于思路3的初始代码简化而来的。

版本1:

//思路3的代码简化
class Solution {
    public int largestRectangleArea(int[] heights) {
        if(heights == null || heights.length == 0) return 0;
        int max = 0;
        Stack<Integer> stack = new Stack<>();
        stack.push(-1);
        for(int i = 0; i <= heights.length; i++){
            if(i == heights.length && stack.size() == 1) break;
            //栈内只有一个-1时,则元素入栈
            if(stack.size() == 1) stack.push(i);
            //栈内不止一个-1时,取栈顶与元素i比较,如果i==height.length,构造哨兵target=-1,否则取heights[i]
            int target = i == heights.length ? -1 : heights[i];
            int top = stack.peek();

            if(target >= heights[top]) stack.push(i);
            else{
                //当栈顶对应的元素值 > target,不断取出栈顶作为高计算对应的矩形面积,直到target>=栈顶对应的值
                while(stack.size() > 1 && target < heights[stack.peek()]){
                    int right = i;//元素i下标作为右边界
                    int high = heights[stack.pop()];//弹出栈顶,取出对应的高
                    int left = stack.peek();//顶替上来的下标作为左边界
                    max = Math.max(max, (right - left - 1) * high);
                }
                //将当前元素下标入栈(易忽略!!!)
                if(i < heights.length) stack.push(i);//i==heights.length的哨兵是不需要入栈的
            }
        }
        return max;
    }
}

版本2:(更简洁,推荐)

//思路3的代码简化:这个版本更简洁
class Solution {
    public int largestRectangleArea(int[] heights) {
        if(heights == null || heights.length == 0) return 0;
        Stack<Integer> stack = new Stack<>();//单调栈
        stack.push(-1);//哨兵1
        int max = 0, area = 0;
        for(int i = 0; i <= heights.length; i++){
            if(i == heights.length){//哨兵2
                while(stack.size() > 1){
                    int right = i;
                    int height = heights[stack.pop()];
                    int left = stack.peek();
                    max = Math.max(max,(right - left - 1) * height);
                }
            }
            else{
                while(stack.size() > 1 && heights[stack.peek()] > heights[i]){
                    int right = i;
                    int height = heights[stack.pop()];
                    int left = stack.peek();
                    max = Math.max(max,(right - left - 1) * height);
                }
                //直到栈中没有大于heights[i]的元素时,将当前元素加入栈中(易忽略!!)
                stack.push(i);  
            }
        }
        return max;
    }
}
深度学习是机器学习的一个子领域,它基于人工神经网络的研究,特别是利用多层次的神经网络来进行学习和模式识别。深度学习模型能够学习数据的高层次特征,这些特征对于图像和语音识别、自然语言处理、医学图像分析等应用至关重要。以下是深度学习的一些关键概念和组成部分: 1. **神经网络(Neural Networks)**:深度学习的基础是人工神经网络,它是由多个层组成的网络结构,包括输入层、隐藏层和输出层。每个层由多个神经元组成,神经元之间通过权重连。 2. **前馈神经网络(Feedforward Neural Networks)**:这是最常见的神经网络类型,信息从输入层流向隐藏层,最终到达输出层。 3. **卷积神经网络(Convolutional Neural Networks, CNNs)**:这种网络特别适合处理具有网格结构的数据,如图像。它们使用卷积层来提取图像的特征。 4. **循环神经网络(Recurrent Neural Networks, RNNs)**:这种网络能够处理序列数据,如时间序列或自然语言,因为它们具有记忆功能,能够捕捉数据的时间依赖性。 5. **长短期记忆网络(Long Short-Term Memory, LSTM)**:LSTM 是一种特殊的 RNN,它能够学习长期依赖关系,非常适合复杂的序列预测任务。 6. **生成对抗网络(Generative Adversarial Networks, GANs)**:由两个网络组成,一个生成器和一个判别器,它们相互竞争,生成器生成数据,判别器评估数据的真实性。 7. **深度学习框架**:如 TensorFlow、Keras、PyTorch 等,这些框架提供了构建、训练和部署深度学习模型的工具和库。 8. **激活函数(Activation Functions)**:如 ReLU、Sigmoid、Tanh 等,它们在神经网络用于添加非线性,使得网络能够学习复杂的函数。 9. **损失函数(Loss Functions)**:用于评估模型的预测与真实值之间的差异,常见的损失函数包括均方误差(MSE)、交叉熵(Cross-Entropy)等。 10. **优化算(Optimization Algorithms)**:如梯度下降(Gradient Descent)、随机梯度下降(SGD)、Adam 等,用于更新网络权重,以最小化损失函数。 11. **正则化(Regularization)**:技术如 Dropout、L1/L2 正则化等,用于防止模型过拟合。 12. **迁移学习(Transfer Learning)**:利用在一个任务上训练好的模型来提高另一个相关任务的性能。 深度学习在许多领域都取得了显著的成就,但它也面临着一些挑战,如对大量数据的依赖、模型的解释性差、计算资源消耗大等。研究人员正在不断探索新的方来解决这些问题。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值