问题描述
小R正在处理一个数组序列,他的任务是找出一个区间,使得这个区间的所有数经过以下计算得到的值是最大的:
区间中的最小数 * 区间所有数的和
小R想知道,经过计算后,哪个区间能产生最大的值。你的任务是帮助小R编写一个程序,输出最大计算值。
例如:给定数组序列 [6, 2, 1],可以得到以下区间及其计算值:
- [6] = 6 * 6 = 36
- [2] = 2 * 2 = 4
- [1] = 1 * 1 = 1
- [6, 2] = 2 * 8 = 16
- [2, 1] = 1 * 3 = 3
- [6, 2, 1] = 1 * 9 = 9
根据这些计算,小R可以选定区间 [6],因此输出的最大值为 36。
测试样例
样例1:
输入:n = 3,a = [6, 2, 1] 输出:36
样例2:
输入:n = 4,a = [5, 1, 4, 3] 输出:25
样例3:
输入:n = 5,a = [7, 3, 2, 1, 8] 输出:64
解题思路
解法一、暴力枚举(不推荐,时间复杂度较高)
- 枚举所有可能的区间 [l, r];
- 计算区间和以及区间内的最小值;
- 记录所有计算值中的最大值。
代码实现
public static int solution(int n, int[] a) {
// 初始化最大结果为整数可能的最小值,确保任何数与之比较都会更大
int maxResult = Integer.MIN_VALUE;
// 外层循环,i表示子数组的起始位置
for (int i = 0; i < n; i++) {
// 内层循环,j表示子数组的结束位置
for (int j = i; j < n; j++) {
// 初始化子数组中的最小值为整数可能的最大值,确保任何数与之比较都会更小
int minVal = Integer.MAX_VALUE;
// 初始化子数组的元素和为0
int sum = 0;
// 计算从i到j的子数组的最小值和元素和
for (int k = i; k <= j; k++) {
// 更新子数组中的最小值
minVal = Math.min(minVal, a[k]);
// 累加子数组的元素和
sum += a[k];
}
// 更新最大结果,计算子数组的最小值乘以元素和,并与当前最大结果比较
maxResult = Math.max(maxResult, minVal * sum);
}
}
// 返回计算出的最大结果
return maxResult;
}
时间复杂度:
- 枚举区间:
- 计算区间和:
- 总时间复杂度:
空间复杂度:
解法二、前缀和 + 单调栈
核心思想
通过单调栈优化最小值的寻找,并使用前缀和快速计算区间和。
1. 最小值优化:
利用单调递增栈找到每个元素作为最小值时的左右边界。
- 左边界:第一个小于当前元素的左侧元素位置。
- 右边界:第一个小于当前元素的右侧元素位置。 这确保了每个元素在它作为最小值时的有效区间范围。
2. 前缀和优化区间和计算:
使用前缀和快速计算区间和:
区间和 = prefix[r + 1] − prefix[l]
实现步骤
- 首先构建前缀和数组,用于快速计算区间和;
- 使用单调栈找到每个元素的左右边界;
- 遍历每个元素作为最小值的区间,计算对应的值并更新最大值。
代码实现
public static int solution(int n, int[] a) {
// 前缀和计算,用于快速计算任意子数组的和
int[] prefixSum = new int[n + 1];
for (int i = 0; i < n; i++) {
// 计算前缀和,prefixSum[i+1]表示从数组开头到a[i]的总和
prefixSum[i + 1] = prefixSum[i] + a[i];
}
// 初始化左右边界数组,用于存储每个元素左右两侧小于等于当前元素的位置
int[] left = new int[n];
int[] right = new int[n];
Stack<Integer> stack = new Stack<>();
// 使用单调栈寻找左边界
for (int i = 0; i < n; i++) {
// 当栈不为空且栈顶元素对应的值大于等于当前元素时,弹出栈顶元素
while (!stack.isEmpty() && a[stack.peek()] >= a[i]) {
stack.pop();
}
// 如果栈为空,说明左侧没有小于等于当前元素的值,否则栈顶元素即为左边界
left[i] = stack.isEmpty() ? -1 : stack.peek();
// 当前元素的下标入栈
stack.push(i);
}
// 清空栈,为寻找右边界做准备
stack.clear();
// 使用单调栈寻找右边界
for (int i = n - 1; i >= 0; i--) {
// 当栈不为空且栈顶元素对应的值大于等于当前元素时,弹出栈顶元素
while (!stack.isEmpty() && a[stack.peek()] >= a[i]) {
stack.pop();
}
// 如果栈为空,说明右侧没有小于等于当前元素的值,否则栈顶元素即为右边界
right[i] = stack.isEmpty() ? n : stack.peek();
// 当前元素的下标入栈
stack.push(i);
}
// 初始化最大结果为整数可能的最小值,用于后续比较
int maxResult = Integer.MIN_VALUE;
for (int i = 0; i < n; i++) {
// 计算以a[i]为高度
int l = left[i] + 1; // 左边界加1
int r = right[i] - 1; // 右边界减1
// 使用前缀和计算子数组的和
int sum = prefixSum[r + 1] - prefixSum[l];
// 计算更新最大结果
maxResult = Math.max(maxResult, a[i] * sum);
}
// 返回计算出的最大值
return maxResult;
}
时间复杂度:
单调栈 + 前缀和 + 主循环
空间复杂度:
用于存储前缀和和边界数组。
学习心得
1. 单调栈的应用:
单调栈是一种极为高效的处理方法,特别适用于解决区间最值问题。它的核心优势在于能够迅速地确定每个元素的左右边界,从而避免了传统暴力解法中的冗长计算过程。通过维护一个单调递增或递减的栈结构,我们可以在对数时间内找到所需的边界信息,大大提升了算法的效率。
2. 前缀和的优化:
在处理区间和问题时,前缀和技巧是一种极为实用的方法。它通过预先计算并存储数组前n项的和,将原本需要线性时间复杂度 的区间和计算降低至常数时间复杂度 。这种优化不仅简化了代码,还显著提高了计算速度,尤其在处理大规模数据时效果尤为显著。
3. 优化策略:
我们的优化策略是将复杂问题分解为两个更为简单的子问题:“寻找最小值”和“计算区间和”。首先,我们利用单调栈来高效地解决最小值查找问题,确保在常数时间内获取任意元素的最小值。接着,我们采用前缀和技巧来优化区间和的计算,使得原本耗时的操作变得瞬间完成。通过这种分而治之的方法,我们不仅简化了问题,还分别对两个子问题进行了优化,从而整体提升了算法的性能。
知识点延伸
1. 最大矩形问题:
类似于直方图最大矩形问题,利用单调栈和边界优化。
2. 区间优化问题:
- 最大子数组和问题。
- 固定长度的最大/最小子数组和问题。