青训营X豆包MarsCode 技术训练营第九课 | 豆包MarsCode AI 刷题

73 阅读15分钟

问题描述

小S最近在分析一个数组 h1,h2,...,hNh1​,h2​,...,hN​,数组的每个元素代表某种高度。小S对这些高度感兴趣的是,当我们选取任意 kk 个相邻元素时,如何计算它们所能形成的最大矩形面积。

对于 kk 个相邻的元素,我们定义其矩形的最大面积为:

R(k)=k×min(h[i],h[i+1],...,h[i+k−1])R(k)=k×min(h[i],h[i+1],...,h[i+k−1])

即,R(k)R(k) 的值为这 kk 个相邻元素中的最小值乘以 kk。现在,小S希望你能帮他找出对于任意 kk,R(k)R(k) 的最大值。


测试样例

样例1:

输入:n = 5, array = [1, 2, 3, 4, 5]
输出:9

样例2:

输入:n = 6, array = [5, 4, 3, 2, 1, 6]
输出:9

样例3:

输入:n = 4, array = [4, 4, 4, 4]
输出:16

题目解析

  • 思路

    • 本题的核心思路是利用单调栈来高效地求解任意 k 个相邻元素所能形成的最大矩形面积。整体分为三步,首先分别计算出每个元素向左和向右能够延伸到的边界索引(也就是找到左右两边第一个比它小的元素的索引),然后根据这些边界索引来计算以每个元素为最小高度时能构成的矩形面积,最后从这些矩形面积中找出最大值。
    • 对于计算每个元素的左右边界,使用两个相似的循环,分别从左到右和从右到左遍历数组,借助栈来实现。在从左到右遍历计算 left 数组时(left[i] 表示第 i 个元素向左能延伸到的最近且比它小的元素的索引),如果栈不为空且栈顶元素对应的数组值大于等于当前元素值,就将栈顶元素弹出,持续这个过程直到栈为空或者栈顶元素对应的数组值小于当前元素值,此时如果栈为空,说明当前元素左侧没有比它小的元素了,将 left[i] 设为 -1,否则设为栈顶元素索引。然后将当前元素索引压入栈中,方便后续处理其他元素。同样的道理,从右到左遍历计算 right 数组(right[i] 表示第 i 个元素向右能延伸到的最近且比它小的元素的索引),只是判断和赋值的逻辑稍作调整。
    • 在得到左右边界数组后,遍历数组计算每个元素对应的矩形面积,矩形的宽度为 right[i] - left[i] - 1(也就是左右边界之间包含当前元素的元素个数),高度为当前元素值 array[i],面积就是两者相乘,不断比较并更新最大面积 maxArea,最终返回这个最大面积值。
  • 图解(以 array = [5, 4, 3, 2, 1, 6] 为例)

    1. 计算 left 数组

      • 初始化栈为空,开始从左到右遍历数组。
      • 对于第一个元素 5,栈为空,将其索引 0 压入栈,left[0] = -1(表示左侧没有更小的元素)。
      • 对于第二个元素 4,栈顶元素 0 对应的 array[0] = 5 大于 4,所以弹出栈顶元素,此时栈为空,left[1] = -1,再将 1 压入栈。
      • 对于第三个元素 3,栈顶元素 1 对应的 array[1] = 4 大于 3,弹出栈顶元素,栈为空,left[2] = -1,将 2 压入栈。
      • 以此类推,最终得到 left 数组为 [-1, -1, -1, -1, -1, 0]
    2. 计算 right 数组

      • 先清空栈,然后从右到左遍历数组。
      • 对于最后一个元素 6,栈为空,right[5] = 6(表示右侧没有更小的元素,这里用数组长度表示边界外),将 5 压入栈。
      • 对于倒数第二个元素 1,栈顶元素 5 对应的 array[5] = 6 大于 1,弹出栈顶元素,栈为空,right[4] = 6,将 4 压入栈。
      • 继续往前,对于元素 2,栈顶元素 4 对应的 array[4] = 1 小于 2right[3] = 4,将 3 压入栈。
      • 最终得到 right 数组为 [1, 2, 3, 4, 6, 6]
    3. 计算最大矩形面积

      • 遍历数组,对于元素 5,宽度 right[0] - left[0] - 1 = 1 - (-1) - 1 = 1,面积 5 * 1 = 5
      • 对于元素 4,宽度 right[1] - left[1] - 1 = 2 - (-1) - 1 = 2,面积 4 * 2 = 8
      • 对于元素 3,宽度 right[2] - left[2] - 1 = 3 - (-1) - 1 = 3,面积 3 * 3 = 9
      • 对于元素 2,宽度 right[3] - left[3] - 1 = 4 - (-1) - 1 = 4,面积 2 * 4 = 8
      • 对于元素 1,宽度 right[4] - left[4] - 1 = 6 - (-1) - 1 = 6,面积 1 * 6 = 6
      • 对于元素 6,宽度 right[5] - left[5] - 1 = 6 - 0 - 1 = 5,面积 6 * 5 = 30
      • 比较这些面积值,最大面积为 9,所以最终返回 9
  • 代码详解

import java.util.Stack;

public class Main {
    public static int solution(int n, int[] array) {
        // Edit your code here
        int[] left = new int[n];
        int[] right = new int[n];
        
        // 初始化 left 数组
        Stack<Integer> stack = new Stack<>();
        for (int i = 0; i < n; i++) {
            while (!stack.isEmpty() && array[stack.peek()] >= array[i]) {
                stack.pop();
            }
            left[i] = stack.isEmpty()? -1 : stack.peek();
            stack.push(i);
        }
        
        // 初始化 right 数组
        stack.clear();
        for (int i = n - 1; i >= 0; i--) {
            while (!stack.isEmpty() && array[stack.peek()] >= array[i]) {
                stack.pop();
            }
            right[i] = stack.isEmpty()? n : stack.peek();
            stack.push(i);
        }
        
        // 计算最大矩形面积
        int maxArea = 0;
        for (int i = 0; i < n; i++) {
            int width = right[i] - left[i] - 1;
            int area = array[i] * width;
            if (area > maxArea) {
                maxArea = area;
            }
        }
        
        return maxArea;
    }

    public static void main(String[] args) {
        // Add your test cases here
        
        System.out.println(solution(5, new int[]{1, 2, 3, 4, 5}) == 9);
    }
}
  • 初始化左右边界数组部分

    • 首先创建了两个长度为 n(数组 array 的长度)的整数数组 left 和 right,用于存储每个元素的左右边界索引。

    • 计算 left 数组

      • 创建一个栈 stack,用于辅助计算。在 for 循环中从左到右遍历数组 array,对于每个元素 array[i]

        • 进入 while 循环,只要栈不为空且栈顶元素对应的数组值(通过 array[stack.peek()] 获取)大于等于当前元素值,就弹出栈顶元素,这样做的目的是找到左侧第一个比当前元素小的元素索引。
        • 当 while 循环结束后,判断栈是否为空,如果为空,说明当前元素左侧没有比它小的元素了,将 left[i] 设为 -1;否则,将 left[i] 设为栈顶元素索引(也就是左侧第一个比它小的元素索引),最后将当前元素索引 i 压入栈中,方便后续处理其他元素。
    • 计算 right 数组

      • 先通过 stack.clear() 清空栈,然后从右到左遍历数组 array,对于每个元素 array[i]

        • 同样有一个 while 循环,只要栈不为空且栈顶元素对应的数组值大于等于当前元素值,就弹出栈顶元素,目的是找到右侧第一个比当前元素小的元素索引。
        • 当 while 循环结束后,判断栈是否为空,如果为空,说明当前元素右侧没有比它小的元素了,将 right[i] 设为 n(这里用数组长度表示边界外);否则,将 right[i] 设为栈顶元素索引,最后将当前元素索引 i 压入栈中。
  • 计算最大矩形面积部分

    • 初始化变量 maxArea 为 0,用于存储最大矩形面积。通过 for 循环遍历数组 array,对于每个元素 array[i]

      • 先计算以当前元素为最小高度时矩形的宽度 width,通过 right[i] - left[i] - 1 计算得到,也就是左右边界之间包含当前元素的元素个数。
      • 然后计算矩形面积 area,通过 array[i] * width 得到,即高度乘以宽度。
      • 通过 if (area > maxArea) 判断当前计算出的面积是否大于已记录的最大面积,如果是,则更新 maxArea 为当前面积值。
  • 返回结果部分

    • 最后,在整个循环结束后,通过 return maxArea 返回计算出的最大矩形面积值。
  • 主函数 main 部分

    • 在 main 方法中,目前只写了一个简单的测试用例输出语句,通过调用 solution 方法传入数组长度和具体数组,并将返回结果与预期结果进行比较(通过 == 判断是否相等),然后将比较得到的布尔值结果输出到控制台,以此来检查 solution 方法在这个特定测试用例下是否能正确返回预期的最大矩形面积值。

知识总结

  • 新知识点梳理

    • 单调栈的应用场景和实现思路:本题利用单调栈来高效地查找每个元素左右两边第一个比它小的元素,明白了单调栈在处理这类需要找到相邻元素中相对大小关系并确定边界的问题时非常有效。其核心是通过维护栈内元素的单调性(本题是单调递增栈,即栈内元素对应的数组值是单调递增的),在遍历数组过程中及时弹出不符合单调性的元素,从而快速确定边界信息。
    • 基于边界信息计算矩形面积的方法:学会了根据左右边界索引来计算以某个元素为最小高度时所能构成的矩形面积,明确了宽度和高度的计算方式以及如何通过它们得到矩形面积,并且知道要遍历所有元素来找出最大的矩形面积,这种基于给定规则计算几何图形面积并求最值的思路在很多算法题目中都有应用。
  • 理解

    • 单调栈就像是一个智能的 “筛选器”,在遍历数组的过程中,它根据元素大小关系动态地调整栈内元素,只保留那些符合单调性质的元素,通过巧妙地利用栈的特性(后进先出)和元素的弹出、压入操作,能够快速定位到我们想要的边界元素。而基于边界计算矩形面积则是将抽象的数组元素高度信息转化为具体的几何图形面积计算,通过合理地确定宽度和高度,运用简单的乘法运算就能得到面积,然后通过比较找出最大值,这种从数据到几何再到最值求解的思维转换很关键。
  • 学习建议(针对入门同学)

    • 对于单调栈,要先从简单的示例入手理解其基本原理和操作流程,可以手动模拟栈的变化过程,比如对于一个给定的简单数组,按照代码中的逻辑一步步地在纸上写出栈的元素进出情况以及对应的左右边界计算结果,感受单调栈是如何工作的。然后多做一些运用单调栈解决的基础题目,像给定一个数组求每个元素左边第一个比它小的元素等简单问题,逐步掌握单调栈在不同场景下的应用方式,并且注意总结在什么类型的题目中适合引入单调栈来优化解题。
    • 在学习基于边界计算矩形面积的方法时,先把矩形面积的计算公式(长乘以宽)与本题中的元素高度和边界索引对应关系搞清楚,通过画图的方式来直观地理解如何根据左右边界确定宽度,再结合元素本身作为高度来计算面积。接着可以改变题目中的数组元素值,重新计算面积,加深对计算过程的熟悉程度,同时尝试自己去思考如何对这种计算面积的方法进行变形或者拓展应用到其他类似的几何相关算法问题中。

学习计划

  • 制定刷题计划

    • 基础入门阶段

      • 目标设定:熟悉常见的数据结构(如数组、栈等)的基本操作以及简单算法思想,掌握本题涉及的单调栈基础应用和基本的面积计算相关逻辑,能够独立完成类似简单难度的题目。
      • 刷题范围:选择以数组操作、栈的简单应用以及简单几何计算相关的题目为主,可以在 LeetCode、牛客网等刷题平台上筛选难度标记为简单的题目,或者在 MarsCode AI 刷题功能中查找对应基础知识点的题目集。
      • 时间安排:每天安排 1 - 2 小时用于刷题,每次刷 2 - 3 道题,确保每道题都认真分析题目要求、理解解题思路、自己动手实现代码并且进行简单的测试验证(可以像本题主函数中那样通过简单的输出对比进行验证),同时记录解题过程中遇到的问题和疑惑点。
      • 总结归纳:每完成 5 - 10 道题后,对涉及的知识点进行总结回顾,比如对比不同题目中栈的使用方式有什么异同,面积计算在不同场景下的变化等,整理出自己的知识点笔记,加深对基础知识点的理解和记忆。
    • 巩固提升阶段

      • 目标设定:掌握中等难度的算法题目,能够灵活运用单调栈解决更复杂的边界查找问题以及处理与几何图形计算结合的多种情况,提高解题效率和代码质量,学会分析不同题目之间的关联和差异,优化解题思路。
      • 刷题范围:拓展到包含单调栈与其他数据结构(如队列、哈希表等)结合应用、更复杂的几何图形相关算法以及需要对单调栈进行适当变形或优化的题目。可以在刷题平台上选择中等难度的题目板块,或者利用 MarsCode AI 刷题的智能推荐功能,根据已掌握的知识点推荐相应的中等难度题目。
      • 时间安排:每周抽出 3 - 4 天,每天安排 2 - 3 小时刷题,每次完成 3 - 5 道题。在刷题过程中,注重思考题目与之前做过的基础题目的联系和区别,尝试多种解题思路,对于做错的题目要详细分析错误原因,记录在错题本上,并及时复习相关知识点进行查漏补缺。
      • 总结归纳:针对每类题型(如单调栈结合其他数据结构的题型、复杂几何计算题型等)进行总结,梳理出这类题型的通用解题模板和注意事项,同时对这段时间内新学习的知识点和解题技巧进行整理,形成自己的知识体系框架,便于复习和回顾。
    • 强化突破阶段

      • 目标设定:攻克高难度题目,深入理解单调栈在复杂场景下的深层次原理和优化策略,能够应对各种变形题目以及将单调栈思想拓展应用到其他未接触过的领域,提升在限时条件下解决复杂问题的能力,为应对竞赛或者面试中的难题做好准备。
      • 刷题范围:挑战高难度的算法竞赛真题、知名企业面试中的算法难题以及具有创新性和综合性的题目,例如那些将单调栈与动态规划、图论等复杂知识点融合的题目。可以参加线上的算法竞赛模拟赛、刷题打卡活动等,或者在 MarsCode AI 刷题中专门挑战高难度题目集。
      • 时间安排:每周安排 2 - 3 天,每天 3 - 4 小时用于高难度刷题。每次完成 1 - 2 道题,因为高难度题目往往需要花费大量时间去分析思考和尝试不同解法,所以要注重解题过程中的思路拓展和方法优化,多参考优秀的解题代码和思路解析,与其他刷题者交流讨论,拓宽自己的思维视野。
      • 总结归纳:整理高难度题目的解题思路、代码实现以及优化过程,形成详细的案例分析笔记,定期回顾这些笔记,总结出解决高难度问题的一般性思维方法和技巧,同时对自己知识体系中的薄弱环节进行重点突破,通过专项练习和深入学习不断完善自己的知识储备和解题能力。
  • 利用错题进行针对性学习

    • 错题整理:建立电子错题本或者纸质错题本,将做错的题目按照知识点模块(如单调栈应用错误、面积计算错误等)、错误类型(思路错误、代码实现错误、细节遗漏等)进行分类整理,详细记录题目内容、自己的错误解法、正确解法以及错误原因分析,并且标注出涉及的知识点和需要重点复习的地方。
    • 定期复习与分析:每周安排固定时间(如周末 1 - 2 小时)复习错题,重新做一遍错题,看是否能够正确解答,如果再次做错,仔细分析是之前的错误原因没有彻底解决,还是又出现了新的问题,对于仍然存在的知识漏洞或者解题思维误区进行重点标记