LeetCode 907-子数组的最小值之和

文章介绍了两种方法解决编程问题,一种是通过模拟遍历数组并维护左右边界来计算以每个元素为最右/最左最小子序列的数目;另一种是利用单调栈技巧,分别找到每个元素左边第一个小于它和右边第一个小于等于它的元素,从而快速求解最小值之和。
摘要由CSDN通过智能技术生成

在这里插入图片描述

方法一:模拟

考虑所有满足以数组 a r r arr arr 中的某个元素 a r r [ i ] arr[i] arr[i] 为最右且最小的元素的子序列个数 C [ i ] C[i] C[i],那么题目所求连续子数组的最小值之和即为 ∑ i = 0 n − 1 a r r [ i ] × C [ i ] \sum_{i=0}^{n-1}arr[i]\times C[i] i=0n1arr[i]×C[i],其中数组 a r r arr arr 的长度为 n n n。我们必须假设当前元素为最右边且最小的元素,这样才可以构造互不相交的子序列,否则会出现多次计算,因为一个数组的最小值可能不唯一。

经过以上思考,我们只需要找到每个以 a r r [ i ] arr[i] arr[i] 为最右且最小的子序列的数目 l e f t [ i ] left[i] left[i] 和以 a r r [ i ] arr[i] arr[i] 为最左且最小的子序列的数目 r i g h t [ i ] right[i] right[i],则以 a r r [ i ] arr[i] arr[i] 为最小元素的子序列的数目合计为 l e f t [ i ] × r i g h t [ i ] left[i]\times right[i] left[i]×right[i]

为了防止重复计算,我们可以设 a r r [ i ] arr[i] arr[i] 左边的元素都必须大于等于 a r r [ i ] arr[i] arr[i] a r r [ i ] arr[i] arr[i] 右边的元素必须严格小于 a r r [ i ] arr[i] arr[i]。这就变成了求最小的下标 j ≤ i j\le i ji,且连续子序列中的元素 a r r [ j ] , a r r [ j + 1 ] , ⋯   , a r r [ i ] arr[j],arr[j+1],\cdots,arr[i] arr[j],arr[j+1],,arr[i] 都大于等于 a r r [ i ] arr[i] arr[i];以及求最大的下标 k > i k > i k>i 满足连续子序列 a r r [ i ] , a r r [ i + 1 ] , ⋯   , a r r [ k ] arr[i],arr[i+1],\cdots,arr[k] arr[i],arr[i+1],,arr[k] 都严格大于 a r r [ i ] arr[i] arr[i]

class Solution {
    public final int MOD = 1000000007;
    public int sumSubarrayMins(int[] arr) {
        long sum = 0;
        for(int i = 0; i < arr.length; i++){
            int left = 1, right = 1;
            while(left <= i && arr[i] < arr[i - left]){                 //以当前元素为最小右边界的连续子序列个数
                left++;
            }
            while((i + right) < arr.length && arr[i] <= arr[i + right]){//以当前元素为最小左边界的连续子序列个数
                right++;
            }
            sum = (sum + (long)arr[i] * left * right) % MOD;            //当前元素对总和的贡献
        }
        return (int)sum;
    }
}

方法二:单调栈

上述即转化为经典的单调栈问题,即求数组中当前元素 x x x 左边第一个小于 x x x 的元素以及右边第一个小于等于 x x x 的元素。

对于数组中每个元素 a r r [ i ] arr[i] arr[i],具体做法如下:

  • 求左边第一个小于 a r r [ i ] arr[i] arr[i] 的元素:从左向右遍历数组,并维护一个单调递增的栈,遍历当前元素 a r r [ i ] arr[i] arr[i],如果遇到当前栈顶的元素大于等于 a r r [ i ] arr[i] arr[i] 则将其弹出,直到栈顶的元素小于 a r r [ i ] arr[i] arr[i],栈顶的元素即为左边第一个小于 a r r [ i ] arr[i] arr[i] 的元素 a r r [ j ] arr[j] arr[j],此时 l e f t [ i ] = i − j left[i]=i-j left[i]=ij
  • 求右边第一个小于等于 a r r [ i ] arr[i] arr[i] 的元素:从右向左遍历数组,维护一个单调递增的栈,遍历当前元素 a r r [ i ] arr[i] arr[i],如果遇到当前栈顶的元素大于 a r r [ i ] arr[i] arr[i] 则将其弹出,直到栈顶的元素小于等于 a r r [ i ] arr[i] arr[i],栈顶的元素即为右边第一个小于等于 a r r [ i ] arr[i] arr[i] 的元素 a r r [ k ] arr[k] arr[k],此时 r i g h t [ i ] = k − i right[i]=k-i right[i]=ki
  • 连续子数组 a r r [ j ] , a r r [ j + 1 ] , ⋯   , a r r [ k ] arr[j],arr[j+1],\cdots,arr[k] arr[j],arr[j+1],,arr[k] 的最小元素即为 a r r [ i ] arr[i] arr[i],以 a r r [ i ] arr[i] arr[i] 为最小元素的连续子序列的数量为 ( i − j ) × ( k − i ) (i-j)\times(k-i) (ij)×(ki)

根据以上结论可以知道,所有子数组的最小值之和即为 ∑ i = 0 n − 1 a r r [ i ] × l e f t [ i ] × r i g h t [ i ] \sum_{i=0}^{n-1}arr[i]\times left[i]\times right[i] i=0n1arr[i]×left[i]×right[i]。维护单调栈的过程是线性的。

class Solution {
    public final int MOD = 1000000007;
    public int sumSubarrayMins(int[] arr) {
        int n = arr.length;
        long ans = 0;
        Deque<Integer> monoStack = new ArrayDeque<Integer>();
        int[] left = new int[n];
        int right = 0;
        for (int i = 0; i < n; i++) {        //求左边第一个小于arr[i]的元素,等于计算,防止漏掉最小值不唯一的情况
            while (!monoStack.isEmpty() && arr[i] <= arr[monoStack.peek()]) {
                monoStack.pop();
            }
            left[i] = i - (monoStack.isEmpty() ? -1 : monoStack.peek());
            monoStack.push(i);
        }

        monoStack.clear();
        for (int i = n - 1; i >= 0; i--) {   //求右边第一个小于等于arr[i]的元素,等于不计算,防止重复计算
            while (!monoStack.isEmpty() && arr[i] < arr[monoStack.peek()]) {
                monoStack.pop();
            }
            right = (monoStack.isEmpty() ? n : monoStack.peek()) - i;
            ans = (ans + (long) left[i] * right * arr[i]) % MOD;      //求以arr[i]为最小元素的贡献值
            monoStack.push(i);
        }
        return (int) ans;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值