持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第27天,点击查看活动详情
每日刷题 2022.10.28
- leetcode原题链接:leetcode.cn/problems/su…
- 难度:中等
题目
- 给定一个整数数组 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 * 10^4
1 <= arr[i] <= 3 * 10^4
解题思路
- 单调栈+贡献值
- 根据题目含义:需要找到数组中的子数组的最小值之和,那么最直观的做法:就是将所有的子数组全部罗列出来,时间复杂度
o(n^2)
- 其实将所有的子数组都罗列出来发现,我们可以通过找每个值作为最小值
cur
,出现在子数组中的个数num
,(乘法原理)计算得到cur * num
。将数组中每一个元素作为最小值,出现在子数组中的个数相加,就是总的和。 - 这种做法:求每一个值作为最小值,出现在子数组中的个数。也叫做求每个数的贡献值,对于最后的子数组的最小值之和而言,其是由多个这样的值的贡献值加和起来的总和。
单调栈:那么为什么要使用单调栈呢?
- 因为我们需要找到是当前值从左往右、从右往左的第一个最小的值。方向不重要,重要的是:第一个最小的值,这就是单调栈解题的关键信息。
优化
- 使用两个单调栈,整体的思路是非常易懂的。但是空间上耗费比较多,有没有什么更优的办法,能够使空间上的耗费更小一些呢?让自己的代码更优雅一点呢?
- 如何优化解决这两个问题?
- 问题一:如何找到当前元素
cur
,从右往左的第一个最小值 - 问题二:如何找到当前元素
cur
, 从左往右的第一个最小值
- 问题一:如何找到当前元素
- (解决第一个问题)单调递增栈:里面的所有元素都是依次递增的,也就是说栈中的元素的前一个元素就是其在整个数组中,从右往左看,第一个最小的值。(中间比当前元素大的值,都会在当前元素入栈的时候,被弹出)
- (解决第二个问题)单调递增栈中:对于栈中被弹出的元素而言,使其被弹出的元素,就是其找到的从左往右的第一个最小值;而有些数据可能到最后都不会从栈中弹出,因为其根本找不到从左往右第一个比其小的值,那么这时就需要向栈中添加一个最小的且不会存在在数组中的值,作为一个哨兵,帮助还停留在栈中的元素出栈
时间复杂度
- 没有优化之前,使用两个单调栈,时间复杂度:o(n)
- 优化之后,使用一个单调栈,时间复杂度:o(n), 但是空间上优化了很多。
注意点⚠️
- 取
mod
时候,乘法结束后需要取mod
,最后在相加结束后,也需要取mod
,防止数据的溢出。
AC
代码
/**
* @param {number[]} arr
* @return {number}
*/
var sumSubarrayMins = function(arr) {
// 其实使用一个栈就可以解决
// 单调栈:单调递增栈:可以找到右边的最小值 pop
// push: 栈中的前一个元素,就是当前需要入栈的第一个小于的数
arr.push(0);
let stack = [], n = arr.length, ans = 0, mod = 10 ** 9 + 7;
for(let i = 0; i < n; i++) {
while(stack.length != 0 && arr[stack[stack.length - 1]] > arr[i]) {
let cur = stack.pop(), top;
if(stack.length == 0) top = -1;
else top = stack[stack.length - 1];
// console.log(top, cur, arr[cur], i)
let t = (i - cur) * (cur - top) * arr[cur];
ans = (ans + t) % mod;
}
stack.push(i);
}
// console.log(stack)
return ans;
};