持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第30天,点击查看活动详情
第 30 天
子数组的最小值之和
题目
给定一个整数数组 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 中对应的值)大于等于遍历到的当前元素,则不断出栈。 直到找到左边第一个严格小于当前元素的栈顶元素,把它的后一位当做“最大子数组”的左边界。 同理,从右往左遍历,维护一个单调递增的栈,可以求出每个元素的右边界。 注意:
这里需要注意,在求右边界的时候单调栈不是严格递增的, 也就是对于当前元素与栈顶元素相等的情况,我们需要往右扩张, 因为即使两个同样最小的元素在同一个子数组中,它的最小值也是这两个元素, 我们需要且只能计算一次,所以在求左右边界的时候,出栈的判断条件,一个需要包括相等,另一个不需要包括相等。 然后再遍历一次,利用左右边界可以求出以当前元素为最小值的所有连续子数组的个数,继而可以求出每个元素对答案的贡献值。
/**
* @param {number[]} arr
* @return {number}
*/
var sumSubarrayMins = function(arr) {
// 维护一个严格单调递增的栈,栈中存下标
const stack = [];
const left = [];
const right = [];
const len = arr.length;
const mod = 1_000_000_007;
let ans = 0;
for(let i = 0; i < len; i++) {
// 一个需要等于
while(stack.length && arr[stack[stack.length-1]] >= arr[i]) {
stack.pop();
}
const top = stack[stack.length-1] ?? -1;
stack.push(i);
left.push(top + 1);
}
stack.length = 0;
for(let i = len-1; i >= 0; i--) {
// 一个不能等于
while(stack.length && arr[stack[stack.length-1]] > arr[i]) {
stack.pop();
}
const top = stack[stack.length-1] ?? len;
stack.push(i);
right.push(top - 1);
}
right.reverse();
// console.log(left);
// console.log(right);
for(let i = 0; i < len; i++) {
const subArrLen = (i - left[i] + 1) * (right[i] - i + 1);
const contribution = subArrLen % mod * arr[i] % mod;
// console.log(subArrLen, arr[i], contribution);
ans = (ans + contribution) % mod;
}
return ans;
};