题目解析 小B的极差之和 | 豆包MarsCode AI 刷题

题目链接

小B的极差之和

问题描述

小B拥有一个数组 a,她使用这个数组构造了一个新数组 b。其中,a[i] 表示在新数组 b 中有 a[i]i+1。例如,若 a = [2, 3, 1],那么新数组 b = [1, 1, 2, 2, 2, 3],因为 a[0] = 2 代表数组 b 中有 2 个 1a[1] = 3 代表数组 b 中有 3 个 2a[2] = 1 代表数组 b 中有 1 个 3

现在,你需要帮助小B求出 b 数组中所有连续子数组的极差之和。由于答案可能非常大,请对 10^9+7 取模。

数组的极差定义为子数组的最大值减去最小值。


测试样例

样例1:

输入:n = 2,a = [2, 1]

输出:2

样例2:

输入:n = 3,a = [1, 2, 1]

输出:6

方法1: 暴力法

最直观的方法。通过遍历数组 b 的每个可能的连续子数组,计算它们的极差并累加到总和中。

主要步骤:

  1. 构建数组 b:遍历输入数组 a,根据 a[i] 的值将 i+1 插入到 b 数组中。

  2. 遍历 b 的所有连续子数组:使用两层循环,外层确定子数组起始位置,内层扩展子数组的终止位置。

  3. 在每次扩展时,更新当前子数组的最大值和最小值,并计算极差。

  4. 将极差累加到总和中,并在每次累加时取模,以防止溢出。

代码实现

java 复制代码
// 暴力法
import java.util.ArrayList;
import java.util.List;

public class Main {
    private static final int MODULO = 1000000007; // 模

    public static int solution(int n, int[] a) {
        
        // 构建b
        List<Integer> constructedList = new ArrayList<>();
        
        //  b
        for (int i = 0; i < n; i++) {
            for (int count = 0; count < a[i]; count++) {
                constructedList.add(i + 1);
            }
        }

        // 极差之和
        int totalSum = 0;
        for (int start = 0; start < constructedList.size(); start++) {
            int minValue = constructedList.get(start);
            int maxValue = constructedList.get(start);
            for (int end = start; end < constructedList.size(); end++) {
                minValue = Math.min(minValue, constructedList.get(end));
                maxValue = Math.max(maxValue, constructedList.get(end));
                totalSum = (totalSum + (maxValue - minValue)) % MODULO;
            }
        }

        return totalSum;
    }

    public static void main(String[] args) {
        int[] array1 = {2, 1};
        int[] array2 = {1, 2, 1};
        int[] array3 = {2, 3, 1, 1};

        System.out.println(solution(2, array1)); // 应输出 2
        System.out.println(solution(3, array2)); // 应输出 6
        System.out.println(solution(4, array3)); // 应输出 26
    }
}

复杂度

时间复杂度O(n^2)。因为需要遍历数组 b 的所有连续子数组。

空间复杂度O(m) 。整体空间复杂度主要由列表 b 决定


方法2:贡献法 + 单调栈

生成数组 b 之后,就需要计算每一个极差了。需要确定每个元素的左边界右边界 是为了计算它在所有可能的子数组中作为最大值最小值 的贡献。具体来说,左右边界帮助我们确定每个元素 b[i] 在多少个子数组中能作为极值出现。

解题思路

  1. 与方法1相同。
  2. 遍历 b 的所有连续子数组:使用两层循环,外层确定子数组起始位置,内层扩展子数组的终止位置。
  • 通过两个单调栈分别计算每个元素在 b 中作为最大值和最小值的左右边界。

  • 左边界和右边界的定义是:左边界是当前元素左侧第一个比它大的或小的元素位置,右边界是当前元素右侧第一个比它大的或小的元素位置。

  1. 贡献:
  • 对于每个元素 b[i],其作为最大值的贡献是 (i - leftGreater[i]) * (rightGreater[i] - i) * b[i]。这个公式表示 b[i] 能作为最大值的子数组数量乘以 b[i] 的值。

  • 其作为最小值的贡献是 (i - leftSmaller[i]) * (rightSmaller[i] - i) * b[i]

计算每个元素的最大值和最小值贡献后,求差并累加到 totalSum 中。结果取模以防止溢出。

主要步骤:

leftGreaterrightGreater 数组用于存储每个元素作为最大值时的左边界和右边界。

leftSmallerrightSmaller 数组用于存储每个元素作为最小值时的左边界和右边界。

我们初始化这些数组,将左边界设为 -1(表示起始位置前)和右边界设为 b.length(表示终止位置后)。

找最大值左右值边界时:当我们发现栈顶元素小于当前元素时,当前元素的位置 i 就是栈顶元素作为最大值的右边界。弹出栈顶元素并更新 rightGreater。如果栈不为空,当前元素 i 的左边界是栈顶元素的位置。将当前元素索引 i 压入栈,继续遍历。

找最小值左右边界时:当栈顶元素大于当前元素时,当前元素的位置 i 是栈顶元素作为最小值的右边界。如果栈不为空,当前元素 i 的左边界是栈顶元素的位置。清空栈后,按照同样的逻辑处理最小值的左右边界。

下面是代码实现的示例:

java 复制代码
// 方法2:贡献法 + 单调栈
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

public class Main {
    private static final int MODULO = 1000000007;  // 模

    public static int solution(int n, int[] a) {
        // Step 1: 构建数组 `b`
        int totalLength = 0;
        for (int value : a) {
            totalLength += value;
        }
        int[] b = new int[totalLength];
        int index = 0;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < a[i]; j++) {
                b[index++] = i + 1;
            }
        }

        // Step 2: 使用单调栈找左右边界
        int[] leftGreater = new int[b.length];
        int[] rightGreater = new int[b.length];
        int[] leftSmaller = new int[b.length];
        int[] rightSmaller = new int[b.length];

        for (int i = 0; i < b.length; i++) {
            leftGreater[i] = -1;
            rightGreater[i] = b.length;
            leftSmaller[i] = -1;
            rightSmaller[i] = b.length;
        }

        Stack<Integer> stack = new Stack<>();
        
        // 找最大值的左右边界
        for (int i = 0; i < b.length; i++) {
            while (!stack.isEmpty() && b[stack.peek()] < b[i]) {
                rightGreater[stack.pop()] = i;
            }
            if (!stack.isEmpty()) {
                leftGreater[i] = stack.peek();
            }
            stack.push(i);
        }

        stack.clear();
        // 找最小值的左右边界
        for (int i = 0; i < b.length; i++) {
            while (!stack.isEmpty() && b[stack.peek()] > b[i]) {
                rightSmaller[stack.pop()] = i;
            }
            if (!stack.isEmpty()) {
                leftSmaller[i] = stack.peek();
            }
            stack.push(i);
        }

        // Step 3: 计算贡献
        long totalSum = 0;
        for (int i = 0; i < b.length; i++) {
            long maxContribution = (long) (i - leftGreater[i]) * (rightGreater[i] - i) * b[i];
            long minContribution = (long) (i - leftSmaller[i]) * (rightSmaller[i] - i) * b[i];
            totalSum = (totalSum + maxContribution - minContribution) % MODULO;
        }

        return (int) ((totalSum + MODULO) % MODULO);
    }
}

复杂度

时间复杂度O(n)

小结:

不仅在极差问题中发挥作用,括号匹配、表达式计算等各种问题中都会用到这个来优化算法。

贡献法核心思想是直接分析每个元素在数组中对于所有可能子数组的"贡献"值,而不是逐一遍历每个子数组。这种方法显著减少了重复计算,提高了时间效率,尤其适合这题目中计算涉及子数组属性的总和问题,极差、最小值、最大值等。

遇到思维瓶颈时,用 AI 可以我们快速理清思路,从最基础的角度引导我们编写代码,并提供优化和改进建议。能从简单的实现过渡到复杂的优化,将 AI 的建议与自身理解结合起来形成完善的代码方案。所以,保持开放的心态,利用好 AI 这个工具,让我们在刷题的道路上走得更稳、更远。编程的学习是个持续迭代的过程,不要闭门造车,而是借助一切可以利用的资源不断提升自己。

那么,希望这些解题经验能帮助到你,我们下次再见!

相关推荐
柠檬柠檬7 小时前
Go 语言入门指南:基础语法和常用特性解析 | 豆包MarsCode AI刷题
青训营笔记
用户967136399657 小时前
计算最小步长丨豆包MarsCodeAI刷题
青训营笔记
用户52975799354721 天前
字节跳动青训营刷题笔记2| 豆包MarsCode AI刷题
青训营笔记
clearcold1 天前
浅谈对LangChain中Model I/O的见解 | 豆包MarsCode AI刷题
青训营笔记
夭要7夜宵2 天前
【字节青训营】 Go 进阶语言:并发概述、Goroutine、Channel、协程池 | 豆包MarsCode AI刷题
青训营笔记
用户336901104442 天前
数字分组求和题解 | 豆包MarsCode AI刷题
青训营笔记
dnxb1232 天前
GO语言工程实践课后作业:实现思路、代码以及路径记录 | 豆包MarsCode AI刷题
青训营笔记
用户916357440952 天前
AI刷题-动态规划“DNA序列编辑距离” | 豆包MarsCode AI刷题
青训营笔记
热的棒打鲜橙2 天前
数字分组求偶数和 | 豆包MarsCode AI刷题
青训营笔记
JinY142 天前
Go 语言入门指南:基础语法和常用特性解析 | 豆包MarsCode AI刷题
青训营笔记