[JavaScript / leetcode] 907. 子数组的最小值之和

103 阅读1分钟

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

每日刷题 2022.10.28

题目

  • 给定一个整数数组 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;
};