掘金刷题之路 Day 3 | 豆包MarsCode AI刷题

103 阅读6分钟

最大矩形面积问题

启动MarsCode工具分析题意和样例

好的,让我们来解释一下样例输入和输出。

输入样例

5
1 2 3 4 5

输出样例

9

解释

  1. 输入解释:
    • 第一行 5 表示数组的长度为 5。
    • 第二行 1 2 3 4 5 表示数组的内容为 [1, 2, 3, 4, 5]
  1. 输出解释:
    • 输出 9 表示在所有可能的 k 值中,R(k) 的最大值为 9。

详细计算过程

我们需要计算 R(k) 的最大值,其中 R(k) 定义为 k * min(h[i], h[i + 1], ..., h[i + k - 1])

  • k = 1:
    • R(1) = 1 * min(1) = 1
    • R(1) = 1 * min(2) = 2
    • R(1) = 1 * min(3) = 3
    • R(1) = 1 * min(4) = 4
    • R(1) = 1 * min(5) = 5
    • 最大值为 5
  • k = 2:
    • R(2) = 2 * min(1, 2) = 2
    • R(2) = 2 * min(2, 3) = 4
    • R(2) = 2 * min(3, 4) = 6
    • R(2) = 2 * min(4, 5) = 8
    • 最大值为 8
  • k = 3:
    • R(3) = 3 * min(1, 2, 3) = 3
    • R(3) = 3 * min(2, 3, 4) = 6
    • R(3) = 3 * min(3, 4, 5) = 9
    • 最大值为 9
  • k = 4:
    • R(4) = 4 * min(1, 2, 3, 4) = 4
    • R(4) = 4 * min(2, 3, 4, 5) = 8
    • 最大值为 8
  • k = 5:
    • R(5) = 5 * min(1, 2, 3, 4, 5) = 5
    • 最大值为 5

综上所述,R(k) 的最大值为 9,当 k = 3 时,R(3) = 3 * min(3, 4, 5) = 9

因此,输出为 9

继续提问MarsCode工具看看进一步的解题思路

问题分析

我们需要找到 R(k) 的最大值,其中 R(k) 定义为 k * min(h[i], h[i + 1], ..., h[i + k - 1])。这意味着我们需要在数组中找到一个长度为 k 的子数组,使得 k 乘以该子数组的最小值最大。

思路提示

  1. 暴力解法
    • 最简单的方法是遍历所有可能的 k 值(从 1 到 N),然后对于每个 k,遍历所有可能的起始位置 i,计算 R(k) 并记录最大值。
    • 这种方法的时间复杂度为 O(N^3),对于 N 最大为 10^5 的情况,可能会超时。
  1. 优化思路
    • 我们可以考虑使用滑动窗口和单调栈来优化计算过程。
    • 滑动窗口:固定窗口大小 k,滑动窗口并计算每个窗口的最小值,然后计算 R(k)
    • 单调栈:使用单调栈来快速找到每个元素作为最小值时的最大区间长度,从而计算 R(k)

具体步骤

  1. 滑动窗口
    • 对于每个 k 值,使用滑动窗口来计算每个窗口的最小值。
    • 维护一个滑动窗口的最小值,并在窗口滑动时更新最小值。
  1. 单调栈
    • 使用单调栈来找到每个元素作为最小值时的最大区间长度。
    • 对于每个元素 h[i],找到它作为最小值的最大区间长度 k,然后计算 R(k)

我的思考

暴力的解法AC代码 O(n^3)

这里为了让我们更加理解题目我们首先使用暴力解法进行分析,即模拟样例,根据MarsCode工具分析

我们要把次K都求出来然后进行取最大的。

#include <cstddef>
#include <iostream>
#include <vector>

int solution(int n, std::vector<int> A) {
    int result  = 0;
    for(int k = 1;k<=n;k++){
        for(int i = 0;i<=n-k;i++){
            int sum = A[i];
            for(int j = i;j<i+k;j++){
                sum =  std::min(sum,A[j]);
            }
            result = std::max(result,k*sum);
        }
    }
    return result;
}

int main() {
    // Add your test cases here
    std::vector<int> A_case1 = std::vector<int>{1, 2, 3, 4, 5};
    std::cout << (solution(5, A_case1) == 9) << std::endl;
    return 0;
}

滑动窗口 + 单调队列 优化后的解法 O(n^2)

改进思路
  1. 使用一个双端队列来维护当前窗口中的最小值,队列存储的是数组元素的索引。
  2. 窗口滑动时,更新队列:
    • 移除窗口外的元素。
    • 保持队列单调递增(弹出队尾较大的元素)。
  1. 每次计算当前窗口的最小值时,直接取队首元素。
AC代码
#include <iostream>
#include <vector>
#include <deque>
#include <algorithm>

using namespace std;

int solution(int n, vector<int>& heights) {
    int max_result = 0;

    // 遍历所有可能的 k 值
    for (int k = 1; k <= n; ++k) {
        deque<int> dq; // 双端队列,用于维护当前窗口的最小值
        for (int i = 0; i < n; ++i) {
            // 移除窗口外的元素
            if (!dq.empty() && dq.front() < i - k + 1) {
                dq.pop_front();
            }
            // 维护队列单调性,移除队列中比当前元素大的元素
            while (!dq.empty() && heights[dq.back()] >= heights[i]) {
                dq.pop_back();
            }
            // 将当前元素的索引加入队列
            dq.push_back(i);

            // 如果窗口大小达到 k,计算当前窗口的 R(k)
            if (i >= k - 1) {
                int min_height = heights[dq.front()];
                max_result = max(max_result, k * min_height);
            }
        }
    }

    return max_result;
}

int main() {
    // 输入测试数据
    vector<int> A_case1 = {1, 2, 3, 4, 5};
    cout << solution(5, A_case1) << endl; // 输出 9
    return 0;
}

使用单调栈 O(n)

单调栈解法

单调栈的核心思想是预处理每个元素能覆盖的左右边界,使得这个元素成为区间的最小值。对于数组 ( A ):

  • 左边界:找到每个元素左侧第一个比它小的元素位置。
  • 右边界:找到每个元素右侧第一个比它小的元素位置。

我们把目标转换为是找到所有可能的子数组中,乘积 R(k)的最大值。

R(k) =( A[i]×区间长度)

左边界 ( left[i] )

left[i] 是数组中 A[i]左边第一个比它小的元素的索引。
如果 left[i] = -1,表示 A[i]A[i]A[i] 左边所有元素都比它大。

右边界 ( right[i] )

right[i] 是数组中 A[i]右边第一个比它小的元素的索引。
如果 right[i] = n,表示 A[i]右边所有元素都比它大。

因此,A[i]可以作为最小值的最大覆盖区间是: [left[i]+1,right[i]−1]。区间长度是 right[i]−left[i]−1

所以最后计算结果是:

R(k) =(A[i]×(right[i]−left[i]−1))

#include <iostream>
#include <vector>
#include <stack>
#include <algorithm>

int solution(int n, std::vector<int> A) {
    // 左边界和右边界
    std::vector<int> left(n, -1);   // 左边第一个比当前值小的索引
    std::vector<int> right(n, n);  // 右边第一个比当前值小的索引

    // 计算左边界
    std::stack<int> st;
    for (int i = 0; i < n; ++i) {
        while (!st.empty() && A[st.top()] >= A[i]) {
            st.pop();
        }
        if (!st.empty()) {
            left[i] = st.top();
        }
        st.push(i);
    }

    // 清空栈用于计算右边界
    while (!st.empty()) {
        st.pop();
    }
    for (int i = n - 1; i >= 0; --i) {
        while (!st.empty() && A[st.top()] >= A[i]) {
            st.pop();
        }
        if (!st.empty()) {
            right[i] = st.top();
        }
        st.push(i);
    }

    // 计算结果
    int result = 0;
    for (int i = 0; i < n; ++i) {
        int length = right[i] - left[i] - 1;  // 当前元素能覆盖的区间长度
        result = std::max(result, length * A[i]);
    }

    return result;
}

int main() {
    // 测试用例
    std::vector<int> A_case1 = {1, 2, 3, 4, 5};
    std::cout << (solution(5, A_case1) == 9) << std::endl;  // 期望输出 1 (true)

    std::vector<int> A_case2 = {3, 1, 6, 4, 5, 2};
    std::cout << solution(6, A_case2) << std::endl;  // 期望输出 12

    return 0;
}

总结

问题分类和辨识点

这类题目的核心特征是 滑动窗口子数组的性质计算,通常会涉及以下几个关键点:

  • 窗口大小动态变化
    • 如本题中窗口大小 ( k ) 从 ( 1 ) 到 ( N ) 逐步遍历。
  • 区间内的极值计算
    • 子数组中的最小值或最大值是该类型题目的重要特征。
    • 通常需要高效地计算区间的最小值/最大值。
  • 多窗口值的比较
    • 需要对不同大小的窗口进行比较,找到全局最优解。

辨识点总结:

  • 子数组或子区间问题。
  • 要求高效地计算某种性质(如最小值、最大值、平均值)。
  • 窗口大小可以固定,也可以变化。
  • 通常暴力解法过于复杂,需要优化(如单调队列或滑动窗口)。