最大矩形面积问题
启动MarsCode工具分析题意和样例
好的,让我们来解释一下样例输入和输出。
输入样例
5
1 2 3 4 5
输出样例
9
解释
- 输入解释:
-
- 第一行
5表示数组的长度为 5。 - 第二行
1 2 3 4 5表示数组的内容为[1, 2, 3, 4, 5]。
- 第一行
- 输出解释:
-
- 输出
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) = 1R(1) = 1 * min(2) = 2R(1) = 1 * min(3) = 3R(1) = 1 * min(4) = 4R(1) = 1 * min(5) = 5- 最大值为
5
- k = 2:
-
R(2) = 2 * min(1, 2) = 2R(2) = 2 * min(2, 3) = 4R(2) = 2 * min(3, 4) = 6R(2) = 2 * min(4, 5) = 8- 最大值为
8
- k = 3:
-
R(3) = 3 * min(1, 2, 3) = 3R(3) = 3 * min(2, 3, 4) = 6R(3) = 3 * min(3, 4, 5) = 9- 最大值为
9
- k = 4:
-
R(4) = 4 * min(1, 2, 3, 4) = 4R(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 乘以该子数组的最小值最大。
思路提示
- 暴力解法:
-
- 最简单的方法是遍历所有可能的
k值(从 1 到 N),然后对于每个k,遍历所有可能的起始位置i,计算R(k)并记录最大值。 - 这种方法的时间复杂度为
O(N^3),对于N最大为10^5的情况,可能会超时。
- 最简单的方法是遍历所有可能的
- 优化思路:
-
- 我们可以考虑使用滑动窗口和单调栈来优化计算过程。
- 滑动窗口:固定窗口大小
k,滑动窗口并计算每个窗口的最小值,然后计算R(k)。 - 单调栈:使用单调栈来快速找到每个元素作为最小值时的最大区间长度,从而计算
R(k)。
具体步骤
- 滑动窗口:
-
- 对于每个
k值,使用滑动窗口来计算每个窗口的最小值。 - 维护一个滑动窗口的最小值,并在窗口滑动时更新最小值。
- 对于每个
- 单调栈:
-
- 使用单调栈来找到每个元素作为最小值时的最大区间长度。
- 对于每个元素
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)
改进思路
- 使用一个双端队列来维护当前窗口中的最小值,队列存储的是数组元素的索引。
- 窗口滑动时,更新队列:
-
- 移除窗口外的元素。
- 保持队列单调递增(弹出队尾较大的元素)。
- 每次计算当前窗口的最小值时,直接取队首元素。
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 ) 逐步遍历。
- 区间内的极值计算:
-
- 子数组中的最小值或最大值是该类型题目的重要特征。
- 通常需要高效地计算区间的最小值/最大值。
- 多窗口值的比较:
-
- 需要对不同大小的窗口进行比较,找到全局最优解。
辨识点总结:
- 子数组或子区间问题。
- 要求高效地计算某种性质(如最小值、最大值、平均值)。
- 窗口大小可以固定,也可以变化。
- 通常暴力解法过于复杂,需要优化(如单调队列或滑动窗口)。