447-最大区间乘积问题,带你一步步解决困难题 | 豆包MarsCode AI刷题

176 阅读9分钟

问题描述

小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

一句话题意

找到一个数组的区间,使得区间内的最小值和区间和的乘积最大。

题解

枚举区间端点

最容易想到的解题思路,暴力枚举,枚举区间的左右端点,计算区间的最小值以及区间和。

不多说,代码如下:

#include <bits/stdc++.h>

using namespace std;

int solution(int n, std::vector<int> a) {
    int ans = 0;
    
    // 枚举左端点
    for(int l=0;l<n;++l){
        // 枚举右端点
        for(int r=l;r<n;++r){
            // 计算区间最小值和区间和
            int minn = a[l];
            int sum = 0;
            for(int i=l;i<=r;++i){
                minn = min(minn,a[i]);
                sum += a[i];
            } 
            // 更新答案
            ans = max(ans,sum * minn);
        }
    }
    return ans;
}

int main() {
    std::cout << (solution(3, {6, 2, 1}) == 36) << std::endl;
    std::cout << (solution(4, {5, 1, 4, 3}) == 25) << std::endl;
    std::cout << (solution(5, {7, 3, 2, 1, 8}) == 64) << std::endl;
}

暴力方法永远是最简单的,最容易想到的,但是往往也是最慢的,这种方法的时间复杂度是 O(n3)O(n^3),想都不用想,提交肯定超时,交一发试试。

image.png

??? 打脸了,竟然过了。咳咳,这只能说明出题人的样例不够全面,回过头看一下,原来题目也没明确范围是多少。

但是,我们不能仅仅满足于此,一般情况下,这里的n的最大值能够达到1e6这样的数量级,而使用暴力的方法,肯定是无法满足时间要求的。

枚举最小值

那我们其实可以再想一下,难道我们一定要枚举端点吗?因为端点有两个,枚举端点一下就是 n2n^2 ,然后还要再遍历一遍区间计算最小值。最小值,我们仔细想想,要求这个最小值,这个最小值是不是一定得是数组的某个元素。

有了,那我们可以直接枚举这个最小值,相当于枚举数组中的每一个元素当作是最小值,然后反推这个区间,就是再这个最小值x左右两侧找到第一个比x小的元素就是区间的端点。这样一来,时间复杂度一下就降低到了 O(n2)O(n^2)

具体的实现代码如下:

#include <bits/stdc++.h>

using namespace std;

int solution(int n, std::vector<int> a) {
    int ans = 0;
    // 枚举数组的每个元素作为最小值
    for(int i=0;i<n;++i){
        int sum = 0;
        // 计算左侧区间端点
        for(int l=i;l>=0;l--){
            if(a[l] < a[i]) break;
            sum += a[l];
        } 
        // 计算右侧区间端点
        for(int r=i+1;r<n;++r){
            if(a[r] < a[i]) break;
            sum += a[r];
        }
        // 更新答案
        ans = max(ans,a[i]*sum);
    }
    return ans;
}

int main() {
    std::cout << (solution(3, {6, 2, 1}) == 36) << std::endl;
    std::cout << (solution(4, {5, 1, 4, 3}) == 25) << std::endl;
    std::cout << (solution(5, {7, 3, 2, 1, 8}) == 64) << std::endl;
}

不用说,提交,肯定是通过。毕竟上面那个 n3n^3 的算法都能过(小声bb)。

但是如果 nn 的范围达到了1e6,那么这个方案显然也是无法满足要求的。我们还需要考虑怎么再进行一个优化。

单调栈优化

我们再仔细思考之前的方案,枚举数组元素作为最小值,然后求区间。求区间这个动作我仍然需要遍历一次数组,那我能不能直接得到这个区间的端点呢?

求这个区间端点其实就是求对于数组 aa 中下表为 ii 的元素的左右两侧的第一个小于 a[i]a[i] 的元素的下标。这时候有人就说了,这怎么求啊,我也不会啊?

这就很简单了,这刷题工具的旁边不是就有AI吗?直接提问:

怎么在O(1)的时间复杂度内知道数组右侧第一个小于当前元素的值的下标

AI解答:

要在O(1)的时间复杂度内知道数组右侧第一个小于当前元素的值的下标,通常需要预处理数组。可以使用单调栈(Monotonic Stack)来实现这一目标。单调栈是一种数据结构,可以在O(n)的时间复杂度内预处理数组,使得后续查询可以在O(1)的时间复杂度内完成。

  1. 预处理阶段

    • 使用一个单调递增栈来存储数组元素的下标。
    • 遍历数组,对于每个元素,如果栈顶元素对应的数组值大于或等于当前元素,则弹出栈顶元素,直到栈为空或者栈顶元素对应的数组值小于当前元素。
    • 弹出栈顶元素时,记录当前元素的下标为栈顶元素的右侧第一个小于它的元素的下标。
    • 将当前元素的下标压入栈中。
  2. 查询阶段

    • 预处理完成后,每个元素的右侧第一个小于它的元素的下标已经被记录下来,可以在O(1)的时间复杂度内查询。
std::vector<int> findNextSmaller(const std::vector<int>& nums) {
    int n = nums.size();
    std::vector<int> nextSmaller(n, -1); // 初始化为-1,表示没有找到
    std::stack<int> stack;

    for (int i = 0; i < n; ++i) {
        while (!stack.empty() && nums[stack.top()] > nums[i]) {
            nextSmaller[stack.top()] = i;
            stack.pop();
        }
        stack.push(i);
    }

    return nextSmaller;
}

解释

  • nextSmaller 数组用于存储每个元素右侧第一个小于它的元素的下标。
  • 使用单调栈来维护一个递增的序列,栈中存储的是数组元素的下标。
  • 遍历数组时,如果当前元素小于栈顶元素对应的数组值,则弹出栈顶元素,并记录当前元素的下标为栈顶元素的右侧第一个小于它的元素的下标。
  • 最后,栈中剩余的元素没有找到右侧第一个小于它的元素,因此它们的 nextSmaller 值保持为初始值 -1

使用示例

int main() {
    std::vector<int> nums = {6, 2, 1, 5, 4, 3};
    std::vector<int> nextSmaller = findNextSmaller(nums);

    for (int i = 0; i < nums.size(); ++i) {
        std::cout << "Element " << nums[i] << " has next smaller element at index: " << nextSmaller[i] << std::endl;
    }

    return 0;
}
Element 6 has next smaller element at index: 1
Element 2 has next smaller element at index: 2
Element 1 has next smaller element at index: -1
Element 5 has next smaller element at index: 4
Element 4 has next smaller element at index: 5
Element 3 has next smaller element at index: -1

AI很懂,这下你知道了,要使用单调栈

了解一下单调栈的知识后,直接就可以使用AI给的代码。这个计算的是右侧的第一个小于元素的下标,那怎么计算左侧的呢?其实很简单,倒序遍历一下就可以了!

修改的代码如下:

// 计算的右侧的第一个小于元素的下标
vector<int> findRightNextSmaller(const vector<int>& nums) {
    int n = nums.size();
    vector<int> nextSmaller(n, -1); // 初始化为-1,表示没有找到
    stack<int> stack;

    for (int i = 0; i < n; ++i) {
        while (!stack.empty() && nums[stack.top()] > nums[i]) {
            nextSmaller[stack.top()] = i;
            stack.pop();
        }
        stack.push(i);
    }

    return nextSmaller;
}
// 计算的左侧的第一个小于元素的下标
vector<int> findLeftNextSmaller(const vector<int>& nums) {
    int n = nums.size();
    vector<int> nextSmaller(n, -1); // 初始化为-1,表示没有找到
    stack<int> stack;

    for (int i = n-1; i >= 0; --i) {
        while (!stack.empty() && nums[stack.top()] > nums[i]) {
            nextSmaller[stack.top()] = i;
            stack.pop();
        }
        stack.push(i);
    }

    return nextSmaller;
}

但是这个时候又出现问题了,我要求和啊,那我还不是要遍历一遍整个区间,那不是一样了吗。那有没有方法能直接得到区间的和?遇事不决,AI解决。

提问AI:

怎么在O(1)的时间内计算出数组的区间和

AI回答:

要在O(1)的时间内计算出数组的区间和,可以使用前缀和(Prefix Sum)数组。前缀和数组是一种预处理技术,可以在O(n)的时间复杂度内构建,之后可以在O(1)的时间复杂度内查询任意区间的和。

  1. 预处理阶段

    • 构建一个前缀和数组 prefixSum,其中 prefixSum[i] 表示原数组 nums 从下标 0 到下标 i 的元素之和。
    • 具体来说,prefixSum[i] = nums[0] + nums[1] + ... + nums[i]
  2. 查询阶段

    • 对于任意区间 [l, r],区间和可以通过 prefixSum[r] - prefixSum[l-1] 计算得到。
    • 如果 l 为 0,则区间和为 prefixSum[r]

#include <vector>
#include <iostream>

std::vector<int> buildPrefixSum(const std::vector<int>& nums) {
    int n = nums.size();
    std::vector<int> prefixSum(n, 0);
    prefixSum[0] = nums[0];

    for (int i = 1; i < n; ++i) {
        prefixSum[i] = prefixSum[i - 1] + nums[i];
    }

    return prefixSum;
}

int queryRangeSum(const std::vector<int>& prefixSum, int l, int r) {
    if (l == 0) {
        return prefixSum[r];
    } else {
        return prefixSum[r] - prefixSum[l - 1];
    }
}

int main() {
    std::vector<int> nums = {6, 2, 1, 5, 4, 3};
    std::vector<int> prefixSum = buildPrefixSum(nums);

    // 查询区间 [1, 3] 的和
    int sum = queryRangeSum(prefixSum, 1, 3);
    std::cout << "Sum of range [1, 3]: " << sum << std::endl;

    return 0;
}

解释

  • buildPrefixSum 函数用于构建前缀和数组。
  • queryRangeSum 函数用于查询任意区间的和。
  • 在 main 函数中,首先构建前缀和数组,然后查询指定区间的和并输出。

哦,简单!原来是使用前缀和

这下所有的问题都解决了。直接上代码。

#include <bits/stdc++.h>

using namespace std;
// 计算的右侧的第一个小于元素的下标
vector<int> findRightNextSmaller(const vector<int>& nums) {
    int n = nums.size();
    vector<int> nextSmaller(n, -1); // 初始化为-1,表示没有找到
    stack<int> stack;

    for (int i = 0; i < n; ++i) {
        while (!stack.empty() && nums[stack.top()] > nums[i]) {
            nextSmaller[stack.top()] = i;
            stack.pop();
        }
        stack.push(i);
    }

    return nextSmaller;
}
// 计算的左侧的第一个小于元素的下标
vector<int> findLeftNextSmaller(const vector<int>& nums) {
    int n = nums.size();
    vector<int> nextSmaller(n, -1); // 初始化为-1,表示没有找到
    stack<int> stack;

    for (int i = n-1; i >= 0; --i) {
        while (!stack.empty() && nums[stack.top()] > nums[i]) {
            nextSmaller[stack.top()] = i;
            stack.pop();
        }
        stack.push(i);
    }

    return nextSmaller;
}
// 计算前缀和
vector<int> buildPrefixSum(const std::vector<int>& nums) {
    int n = nums.size();
    std::vector<int> prefixSum(n, 0);
    prefixSum[0] = nums[0];

    for (int i = 1; i < n; ++i) {
        prefixSum[i] = prefixSum[i - 1] + nums[i];
    }

    return prefixSum;
}

// 求区间和
int queryRangeSum(const std::vector<int>& prefixSum, int l, int r) {
    if (l == 0) {
        return prefixSum[r];
    } else {
        return prefixSum[r] - prefixSum[l - 1];
    }
}

int solution(int n, std::vector<int> a) {
    // 预处理
    vector<int> sl,sr,presum;
    // 右侧区间下标
    sr = findRightNextSmaller(a);
    // 左侧区间下标
    sl = findLeftNextSmaller(a);
    // 前缀和
    presum = buildPrefixSum(a);

    int ans = 0;
    // 枚举最小值
    for(int i=0;i<n;++i){
        // 区间左端点
        int l = sl[i] == -1 ? 0 : sl[i] + 1;
        // 区间右端点
        int r = sr[i] == -1 ? n-1 : sr[i] - 1;
        // 区间和
        int sum = queryRangeSum(presum,l,r);
        // 更新最大值
        ans = max(ans,a[i]*sum);
    }
    return ans;
}

int main() {
    std::cout << (solution(3, {6, 2, 1}) == 36) << std::endl;
    std::cout << (solution(4, {5, 1, 4, 3}) == 25) << std::endl;
    std::cout << (solution(5, {7, 3, 2, 1, 8}) == 64) << std::endl;
}

这样,由于我们只枚举了最小值,区间端点的计算以及区间和的计算都是直接得到的,所以一个 O(n)O(n) 的算法就完成了,这题也算是解决了,从最开始的 O(n3)O(n^3) 逐步降低复杂度到 O(n)O(n) 。遇到难题,一上来想不到最好的解决方案是正常的,可以从最简单的思路开始想,然后一步步不断地优化算法,遇到不会的问题,还可以问问你身边的刷题助手豆包MarsCode AI。它说不定能够给你很好的启发。