算法--子数组的最小值之和

104 阅读1分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第20天,点击查看活动详情

题目

leetcode 907. 子数组的最小值之和 难度:中等

给定一个整数数组 arr,找到 min(b) 的总和,其中 b 的范围为 arr 的每个(连续)子数组。

由于答案可能很大,因此 返回答案模 10^9 + 7 。

 

示例 1:

输入:arr = [3,1,2,4]
输出:17
解释:
子数组为 [3][1][2][4][3,1][1,2][2,4][3,1,2][1,2,4][3,1,2,4]。 
最小值为 3,1,2,4,1,1,2,1,1,1,和为 17。

示例 2:

输入:arr = [11,81,94,43,3]
输出:444

提示:

1 <= arr.length <= 3 * 104

1 <= arr[i] <= 3 * 104

题解

暴力做法是枚举所有子数组,算出每个子数组的最小值,但这太慢了。

不妨换个视角,对每个数,算出它是哪些子数组的最小值。

例如arr=[1,4,2,3,1],其中 22 是子数组 [2],[4,2],[2,3],[4,2,3][2],[4,2],[2,3],[4,2,3] 的最小值,那么 2 对答案的贡献就是 2⋅4=8。

由于以 2 为最小值的子数组,绝对不能包含比 2 小的数字,因此我们可以找到 2 左右两侧比它小的数的下标,从而确定子数组的边界。2 对应的边界为开区间 (0,4),即闭区间 [1,3],只要在闭区间 [1,3] 范围内且包含下标 2 的子数组,就是以 2 为最小值的子数组。

var sumSubarrayMins = function(arr) {
    const n = arr.length;
    let monoStack = [];
    const left = new Array(n).fill(0);
    const right = new Array(n).fill(0);
    for (let i = 0; i < n; i++) {
        while (monoStack.length !== 0 && arr[i] <= arr[monoStack[monoStack.length - 1]]) {
            monoStack.pop();
        }
        left[i] = i - (monoStack.length === 0 ? -1 : monoStack[monoStack.length - 1]);
        monoStack.push(i);
    }
    monoStack = [];
    for (let i = n - 1; i >= 0; i--) {
        while (monoStack.length !== 0 && arr[i] < arr[monoStack[monoStack.length - 1]]) {
            monoStack.pop();
        }
        right[i] = (monoStack.length === 0 ? n : monoStack[monoStack.length - 1]) - i;
        monoStack.push(i);
    }
    let ans = 0;
    const MOD = 1000000007;
    for (let i = 0; i < n; i++) {
        ans = (ans + left[i] * right[i] * arr[i]) % MOD; 
    }
    return ans;
};

代码详解

[3, 1, 2, 4]从第一个开始往右遍历,遇到小的就替换 [3, 1, 1, 1] [0, 1, 1, 1]从第二个开始 [0, 0, 2, 2]第三 [0, 0, 0, 4]第四 遍历出来每个数组的和就是结果; 以第一个数组为例,实际对应的子数组是[3],[3,1],[3,1,2],[3,1,2,4] 第二个数组是从第二位开始,对应的是[1],[1,2],[1,2,4] ...