刷题:447、最大区间乘积问题 | 豆包MarsCode AI刷题

58 阅读6分钟

问题描述

小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;
    }

时间复杂度:O(n3)O(n^3)

  • 枚举区间:O(n2)O(n^2)
  • 计算区间和:O(n)O(n)
  • 总时间复杂度:O(n3)O(n^3)

空间复杂度:O(1)O(1)

解法二、前缀和 + 单调栈

核心思想

通过单调栈优化最小值的寻找,并使用前缀和快速计算区间和。

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;
    }

时间复杂度:O(n)O(n)

单调栈 O(n)O(n) + 前缀和 O(n)O(n) + 主循环 O(n)O(n)

空间复杂度:O(n)O(n)

用于存储前缀和和边界数组。

学习心得

1. 单调栈的应用:

单调栈是一种极为高效的处理方法,特别适用于解决区间最值问题。它的核心优势在于能够迅速地确定每个元素的左右边界,从而避免了传统暴力解法中的冗长计算过程。通过维护一个单调递增或递减的栈结构,我们可以在对数时间内找到所需的边界信息,大大提升了算法的效率。

2. 前缀和的优化:

在处理区间和问题时,前缀和技巧是一种极为实用的方法。它通过预先计算并存储数组前n项的和,将原本需要线性时间复杂度 O(n)O(n) 的区间和计算降低至常数时间复杂度 O(1)O(1)。这种优化不仅简化了代码,还显著提高了计算速度,尤其在处理大规模数据时效果尤为显著。

3. 优化策略:

我们的优化策略是将复杂问题分解为两个更为简单的子问题:“寻找最小值”和“计算区间和”。首先,我们利用单调栈来高效地解决最小值查找问题,确保在常数时间内获取任意元素的最小值。接着,我们采用前缀和技巧来优化区间和的计算,使得原本耗时的操作变得瞬间完成。通过这种分而治之的方法,我们不仅简化了问题,还分别对两个子问题进行了优化,从而整体提升了算法的性能。

知识点延伸

1. 最大矩形问题:

类似于直方图最大矩形问题,利用单调栈和边界优化。

2. 区间优化问题:

  • 最大子数组和问题。
  • 固定长度的最大/最小子数组和问题。