LeetCode907:子数组的最小值之和

50 阅读5分钟

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

题目描述

难度:中等

给定一个整数数组 arr,找到 min(b) 的总和,其中 b 的范围为 arr 的每个连续子数组。 由于答案可能很大,因此返回答案模10^9 + 7

输入:数组arr
输出:所有子数组中最小值的和

算法

  • 分析题目,每个子数组的最小值都是整个数组中的某个数,最终结果就是数组中各数与其成为最小值次数乘积的和,因此转换为求数组中每个元素成为子数组中最小值的次数。
  • 若某个元素是子数组中的最小值,则其所在的最长子数组一定是:左边界的左侧第一个元素小于当前元素,右边界的右侧第一个元素小于当前元素。然后以最小值为中心,分别收缩左边界和右边界。当前元素成为子数组中最小值的次数,就是以当前元素为最小值的子区间的个数。
  • 假设最小值到最长子区间左侧的元素个数为left,到最长子区间右侧的元素个数为right,则以当前元素为最小值的子区间的个数就为left * right。
  • left * right的含义:
  1. 以最小值为中心,首先依次将左侧的left个元素归入区间,能够构成的子数组个数为left;
  2. 然后将左侧元素清空,待将右侧第一个元素归入区间后,再依次将左侧的left个元素归入区间,此时能够构成的子数组个数为left + left;
  3. 重复第二步,直到右侧的right个元素有全部归入区间,此时的区间等于最小值元素所能构成的以它为最小值的最长区间;
  4. 第二步要循环right次,因此一共能构成的子数组个数为left * right。
  • 题目最终转换成求每个元素左右两侧比当前元素小的元素的下标。

方法一:左右延伸寻找最小值

首先遍历数组中的元素,对于每个元素,分别向左向右再次遍历数组,寻找第一个小于当前元素的元素的下标。
时间复杂度:两层的嵌套循环,时间复杂度为O(n^2)
空间复杂度:只使用了l,r,lc,rc等常数个变量来记录位置,空间复杂度为O(1)

方法二:单调栈 + 两次遍历

方法一虽然能够解决问题,并且空间复杂度很小,但时间复杂度较高,可能出现超时,而求两侧第一个比当前元素大/小的元素,最佳方案应联想到单调栈。
本题使用两次遍历 + 一个单调栈寻找,并将每个元素所求的下标保存在left和right数组中:
从左到右遍历:构建单调递增栈,寻找左边比当前元素小的元素
从右到左遍历:构建单调递增栈,寻找右边比当前元素小的元素 注:第一次遍历后使用deque.clear()清空第一次遍历的结果

时间复杂度:三次长度为n的遍历,时间复杂度为O(n)
空间复杂度:两个长度为n的数组,一个可能的最大长度为n的栈,空间复杂度为O(n)

实现细节

数组中重复元素处理

当数组中有两个相等元素,某个元素在求小值的过程中遍历到另一个相等元素时,是作为当前元素的小值还是大值处理呢?

  • 设想若作为大值处理,将相等元素一齐纳入子数组中,此子数组的最小值仍为当前元素,因此第一个相等元素遍历时,可以将另一相等元素作为大值纳入;
  • 但是,当第二个相等元素遍历时,若仍将第一个相等元素纳入,则纳入后的子数组将会与第一个相等元素纳入第二个相等元素时的子数组重复,造成结果重复计算
  • 因此在求左/右边界(即向左/右遍历)时,将重复元素作为大值处理,而在求右/左边界(即向右/左遍历)时,将重复元素作为小值处理,规避重复,同时不造成遗漏

单调栈

递增栈时:

  • 若栈顶元素大于当前元素,则弹出栈顶,直到栈顶元素小于当前元素,这一过程相当于向左遍历寻找比当前元素小的第一个元素;
  • 若栈顶元素小于当前元素,则栈顶元素就是向左遍历时寻找到的比当前元素小的第一个元素。 记法:(求)最小(值)单调增(栈),(求)最大(值)单调减(栈)

题目要求对结果模10^9 + 7,即设置使用final设置一个全局不变的常量MOD=100000007,每次ans叠加后将ans模MOD来仅保存余数。同时考虑每次乘法和加法后可能出现溢出,及时将int转换为long。

代码实现

// 方法一
public int sumSubarrayMins(int[] arr) {
    final int MOD = 1000000007;
    long ans = 0;
    int n = arr.length;
    for(int i = 0; i < n; i++){
        int lc = i - 1, rc = i + 1;
        int l, r;
        while(lc >= 0 && arr[lc] >= arr[i]){
            lc--;
        }
        l = (lc >= 0) ? (i - lc - 1) : i;
        while(rc < n && arr[rc] > arr[i]){
            rc++;
        }
        r = (rc < n) ? (rc - i - 1) : (n - i - 1);
        ans += (long)arr[i] * (l + r + l * r + 1);
        ans = ans % MOD;
    }
    return (int)ans;
}

// 方法二
public int sumSubarrayMins(int[] arr) {
    final int MOD = 1000000007;
    long ans = 0;
    int n = arr.length;
    int[] left = new int[n], right = new int[n];
    Deque<Integer> deque = new ArrayDeque<>();
    
    for(int i = 0; i < n; i++){
        while(!deque.isEmpty() && arr[i] < arr[deque.peek()]){
            deque.pop();
        }
        left[i] = i - (deque.isEmpty() ? -1 : deque.peek()); 
        deque.push(i);
    }
    deque.clear();
    
    for(int i = n - 1; i >= 0; i--){
        while(!deque.isEmpty() && arr[i] <= arr[deque.peek()]){
            deque.pop();
        }
        if(!deque.isEmpty()){
            right[i] = deque.peek() - i;
        }else{
            right[i] = n - i;
        }
        right[i] = (deque.isEmpty() ? n : deque.peek()) - i; 
        deque.push(i);
    }
    
    for(int i = 0; i < n; i++){
        ans = (ans + (long)arr[i] * left[i] * right[i]) % MOD;
    }
    return (int)ans;
}