单调栈题目:子数组的最小值之和

题目

标题和出处

标题:子数组的最小值之和

出处:907. 子数组的最小值之和

难度

7 级

题目描述

要求

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

示例

示例 1:

输入: arr   =   [3,1,2,4] \texttt{arr = [3,1,2,4]} arr = [3,1,2,4]
输出: 17 \texttt{17} 17
解释:
子数组为 [3] \texttt{[3]} [3] [1] \texttt{[1]} [1] [2] \texttt{[2]} [2] [4] \texttt{[4]} [4] [3,1] \texttt{[3,1]} [3,1] [1,2] \texttt{[1,2]} [1,2] [2,4] \texttt{[2,4]} [2,4] [3,1,2] \texttt{[3,1,2]} [3,1,2] [1,2,4] \texttt{[1,2,4]} [1,2,4] [3,1,2,4] \texttt{[3,1,2,4]} [3,1,2,4]
最小值为 3 \texttt{3} 3 1 \texttt{1} 1 2 \texttt{2} 2 4 \texttt{4} 4 1 \texttt{1} 1 1 \texttt{1} 1 2 \texttt{2} 2 1 \texttt{1} 1 1 \texttt{1} 1 1 \texttt{1} 1
和为 17 \texttt{17} 17

示例 2:

输入: arr   =   [11,81,94,43,3] \texttt{arr = [11,81,94,43,3]} arr = [11,81,94,43,3]
输出: 444 \texttt{444} 444

数据范围

  • 1 ≤ arr.length ≤ 3 × 10 4 \texttt{1} \le \texttt{arr.length} \le \texttt{3} \times \texttt{10}^\texttt{4} 1arr.length3×104
  • 1 ≤ arr[i] ≤ 3 × 10 4 \texttt{1} \le \texttt{arr[i]} \le \texttt{3} \times \texttt{10}^\texttt{4} 1arr[i]3×104

解法

思路和算法

假设数组 arr \textit{arr} arr 的长度是 n n n,则对于 1 ≤ l ≤ n 1 \le l \le n 1ln,数组 arr \textit{arr} arr 的长度 l l l 的子数组有 n − l + 1 n - l + 1 nl+1 个,所有长度的子数组有 n ( n + 1 ) 2 \dfrac{n(n+1)}{2} 2n(n+1) 个。如果直接遍历每个子数组寻找最小值,则子数组的数量是 O ( n 2 ) O(n^2) O(n2),每个子数组需要 O ( n ) O(n) O(n) 的时间遍历,总时间复杂度是 O ( n 3 ) O(n^3) O(n3),该时间复杂度过高,需要优化。

为了得到子数组的最小值之和,对于下标 i i i,需要找到最大的下标 j j j 和最小的下标 k k k,满足 j < i < k j < i < k j<i<k arr [ j ] ≤ arr [ i ] \textit{arr}[j] \le \textit{arr}[i] arr[j]arr[i] arr [ k ] ≤ arr [ i ] \textit{arr}[k] \le \textit{arr}[i] arr[k]arr[i],则子数组 arr [ j + 1 : k − 1 ] \textit{arr}[j + 1 : k - 1] arr[j+1:k1](表示数组 arr \textit{arr} arr 从下标 j + 1 j + 1 j+1 到下标 k − 1 k - 1 k1 的子数组,包含下标 j + 1 j + 1 j+1 和下标 k − 1 k - 1 k1)的最小值是 arr [ i ] \textit{arr}[i] arr[i],子数组 arr [ j + 1 : k − 1 ] \textit{arr}[j + 1 : k - 1] arr[j+1:k1] 的任何一个包含下标 i i i 的子数组的最小值也是 arr [ i ] \textit{arr}[i] arr[i]

由于最小值是 arr [ i ] \textit{arr}[i] arr[i] 的子数组必须包含下标 i i i,其开始下标必须大于等于 j + 1 j + 1 j+1,其结束下标必须小于等于 k − 1 k - 1 k1,因此最小值是 arr [ i ] \textit{arr}[i] arr[i] 的子数组的数量是 ( i − j ) × ( k − i ) (i - j) \times (k - i) (ij)×(ki)。在子数组的最小值之和中, arr [ i ] \textit{arr}[i] arr[i] 被计算的次数是 ( i − j ) × ( k − i ) (i - j) \times (k - i) (ij)×(ki),因此 arr [ i ] \textit{arr}[i] arr[i] 对子数组的最小值之和的贡献值是 arr [ i ] × ( i − j ) × ( k − i ) \textit{arr}[i] \times (i - j) \times (k - i) arr[i]×(ij)×(ki)

特别地,当下标 i i i 左边的元素都大于 arr [ i ] \textit{arr}[i] arr[i] j = − 1 j = -1 j=1,当下标 i i i 右边的元素都大于 arr [ i ] \textit{arr}[i] arr[i] k = n k = n k=n。这道题中可以假设 arr [ − 1 ] = arr [ n ] = − ∞ \textit{arr}[-1] = \textit{arr}[n] = -\infty arr[1]=arr[n]=

上面的描述中, arr [ j ] ≤ arr [ i ] \textit{arr}[j] \le \textit{arr}[i] arr[j]arr[i] arr [ k ] ≤ arr [ i ] \textit{arr}[k] \le \textit{arr}[i] arr[k]arr[i] 的小于等于号也可以改成小于号,虽然每个 arr [ i ] \textit{arr}[i] arr[i] 被计算的次数可能不同,但是所有子数组的最小值之和是相同的。

为了得到每个下标 i i i 对应的下标 j j j k k k,可以使用单调栈。单调栈存储数组 arr \textit{arr} arr 的下标,满足从栈底到栈顶的下标对应的元素单调递增。

从左到右遍历数组 arr \textit{arr} arr,当遍历到下标 i i i 时,如果栈不为空且栈顶下标对应的元素大于等于 arr [ i ] \textit{arr}[i] arr[i],需要计算栈顶下标对应的元素是最小值的子数组的数量和该元素对子数组的最小值之和的贡献值。具体做法如下:

  1. 将栈顶下标出栈,记为 minIndex \textit{minIndex} minIndex,将 arr [ minIndex ] \textit{arr}[\textit{minIndex}] arr[minIndex] 记为 minNum \textit{minNum} minNum,表示当前考虑的子数组最小值;

  2. 在将 minIndex \textit{minIndex} minIndex 出栈之后,记 prevIndex \textit{prevIndex} prevIndex 为对应元素小于等于 minNum \textit{minNum} minNum 的最大下标,如果栈不为空则 prevIndex \textit{prevIndex} prevIndex 为新的栈顶下标,如果栈为空则 prevIndex = − 1 \textit{prevIndex} = -1 prevIndex=1

  3. 子数组 arr [ prevIndex + 1 : i − 1 ] \textit{arr}[\textit{prevIndex} + 1 : i - 1] arr[prevIndex+1:i1] 的最小值是 minNum \textit{minNum} minNum,子数组 arr [ prevIndex + 1 : i − 1 ] \textit{arr}[\textit{prevIndex} + 1 : i - 1] arr[prevIndex+1:i1] 的任何一个包含下标 minIndex \textit{minIndex} minIndex 的子数组的最小值也是 minNum \textit{minNum} minNum,因此在子数组的最小值之和中, minNum \textit{minNum} minNum 被计算的次数是 ( minIndex − prevIndex ) × ( i − minIndex ) (\textit{minIndex} - \textit{prevIndex}) \times (i - \textit{minIndex}) (minIndexprevIndex)×(iminIndex) minNum \textit{minNum} minNum 对子数组的最小值之和的贡献值是 minNum × ( minIndex − prevIndex ) × ( i − minIndex ) \textit{minNum} \times (\textit{minIndex} - \textit{prevIndex}) \times (i - \textit{minIndex}) minNum×(minIndexprevIndex)×(iminIndex),将该贡献值加到子数组的最小值之和;

  4. 如果栈不为空且栈顶下标对应的元素大于等于 arr [ i ] \textit{arr}[i] arr[i],则重复上述 3 步操作,直到栈为空或者栈顶下标对应的元素小于 arr [ i ] \textit{arr}[i] arr[i]

  5. i i i 入栈。

上述操作中,下标 i i i 入栈的条件是栈为空或者栈顶下标对应的元素小于 arr [ i ] \textit{arr}[i] arr[i],因此可以保证从栈底到栈顶的下标对应的元素单调递增。

遍历结束之后,如果单调栈不为空,则需要对栈内的每个下标计算该下标对应的元素在子数组的最小值之和中被计算的次数以及对子数组的最小值之和的贡献值。具体做法如下:

  1. 将栈顶下标出栈,记为 minIndex \textit{minIndex} minIndex,将 arr [ minIndex ] \textit{arr}[\textit{minIndex}] arr[minIndex] 记为 minNum \textit{minNum} minNum,表示当前考虑的子数组最小值;

  2. 在将 minIndex \textit{minIndex} minIndex 出栈之后,记 prevIndex \textit{prevIndex} prevIndex 为对应元素小于等于 minNum \textit{minNum} minNum 的最大下标,如果栈不为空则 prevIndex \textit{prevIndex} prevIndex 为新的栈顶下标,如果栈为空则 prevIndex = − 1 \textit{prevIndex} = -1 prevIndex=1

  3. 子数组 arr [ prevIndex + 1 : n − 1 ] \textit{arr}[\textit{prevIndex} + 1 : n - 1] arr[prevIndex+1:n1] 的最小值是 minNum \textit{minNum} minNum,子数组 arr [ prevIndex + 1 : n − 1 ] \textit{arr}[\textit{prevIndex} + 1 : n - 1] arr[prevIndex+1:n1] 的任何一个包含下标 minIndex \textit{minIndex} minIndex 的子数组的最小值也是 minNum \textit{minNum} minNum,因此在子数组的最小值之和中, minNum \textit{minNum} minNum 被计算的次数是 ( minIndex − prevIndex ) × ( n − minIndex ) (\textit{minIndex} - \textit{prevIndex}) \times (n - \textit{minIndex}) (minIndexprevIndex)×(nminIndex) minNum \textit{minNum} minNum 对子数组的最小值之和的贡献值是 minNum × ( minIndex − prevIndex ) × ( n − minIndex ) \textit{minNum} \times (\textit{minIndex} - \textit{prevIndex}) \times (n - \textit{minIndex}) minNum×(minIndexprevIndex)×(nminIndex),将该贡献值加到子数组的最小值之和。

  4. 重复上述 3 步操作,直到栈为空。

实现时,需要注意两个方面。一是在计算子数组的最小值之和时需要对 1 0 9 + 7 10^9 + 7 109+7 取模,二是使用 long \texttt{long} long 数据类型存储子数组的最小值之和避免溢出,计算结束时转成 int \texttt{int} int 数据类型然后返回。

代码

class Solution {
    public int sumSubarrayMins(int[] arr) {
        final int MODULO = 1000000007;
        Deque<Integer> stack = new ArrayDeque<Integer>();
        long sum = 0;
        int length = arr.length;
        for (int i = 0; i < length; i++) {
            int num = arr[i];
            while (!stack.isEmpty() && arr[stack.peek()] >= num) {
                int minIndex = stack.pop();
                int minNum = arr[minIndex];
                int prevIndex = stack.isEmpty() ? -1 : stack.peek();
                long curSum = (long) minNum * (minIndex - prevIndex) * (i - minIndex) % MODULO;
                sum = (sum + curSum) % MODULO;
            }
            stack.push(i);
        }
        while (!stack.isEmpty()) {
            int minIndex = stack.pop();
            int minNum = arr[minIndex];
            int prevIndex = stack.isEmpty() ? -1 : stack.peek();
            long curSum = (long) minNum * (minIndex - prevIndex) * (length - minIndex) % MODULO;
            sum = (sum + curSum) % MODULO;
        }
        return (int) sum;
    }
}

复杂度分析

  • 时间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组 arr \textit{arr} arr 的长度。需要遍历数组 arr \textit{arr} arr 一次,每个下标最多入栈和出栈各一次。

  • 空间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组 arr \textit{arr} arr 的长度。空间复杂度主要取决于栈空间,栈内元素个数不会超过 n n n

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

伟大的车尔尼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值