【907. 子数组的最小值之和】

来源:力扣(LeetCode)

描述:

  给定一个整数数组 arr,找到 min(b) 的总和,其中 b 的范围为 arr 的每个(连续)子数组。

  由于答案可能很大,因此 返回答案模 109 + 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]。 
最小值为 3124112111,和为 17

示例 2:

输入:arr = [11,81,94,43,3]
输出:444

提示:

  • 1 <= arr.length <= 3 * 104
  • 1 <= arr[i] <= 3 * 104

方法一:单调栈

  考虑所有满足以数组 arr 中的某个元素 arr[i] 为最右且最小的元素的子序列个数 C[i],那么题目要求求连续子数组的最小值之和即为
1

,其中数组 arr 的长度为 n。我们必须假设当前元素为最右边且最小的元素,这样才可以构造互不相交的子序列,否则会出现多次计算,因为一个数组的最小值可能不唯一。

  经过以上思考,我们只需要找到每个元素 arr[i] 以该元素为最右且最小的子序列的数目 left[i],以及以该元素为最左且最小的子序列的数目 right[i],则以 arr[i] 为最小元素的子序列的数目合计为 left[i] × right[i]。当然为了防止重复计算,我们可以设 arr[i] 左边的元素都必须满足小于等于 arr[i], arr[i] 右边的元素必须满足严格小于 arr[i]。当然这就变成求最小的下标 j ≤ i,且连续子序列中的元素 arr[j], arr[j+1], ⋯, arr[i] 都满足大于等于 arr[i],以及最大的下标 k > i 满足连续子序列 arr[i+1], arr[i+1], ⋯, arr[k] 都满足严格大于 arr[i]。上述即转化为经典的单调栈问题,即求数组中当前元素 x 左边第一个小于 x 的元素以及右边第一个小于等于 x 的元素。

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

  • 求左边第一个小于 arr[i] 的元素:从左向右遍历数组,并维护一个单调递增的栈,遍历当前元素 arr[i],如果遇到当前栈顶的元素大于等于 arr[i] 则将其弹出,直到栈顶的元素小于 arr[i],栈顶的元素即为左边第一个小于 arr[i] 的元素 arr[j],此时 left[i] = i − j。

  • 求右边第一个大于等于 arr[i] 的元素:从右向左遍历数组,维护一个单调递增的栈,遍历当前元素 arr[i],如果遇到当前栈顶的元素大于 arr[i] 则将其弹出,直到栈顶的元素小于等于 arr[i],栈顶的元素即为右边第一个小于等于 arr[i] 的元素 arr[k],此时 right[i] = k − i。

  • 连续子数组 arr[j], arr[j+1], ⋯, arr[k] 的最小元素即为 arr[i],以 arr[i] 为最小元素的连续子序列的数量为 (i−j) × (k−i)。

  根据以上结论可以知道,所有子数组的最小值之和即为
2

维护单调栈的过程线性的,因为只进行了线性次的入栈和出栈。

代码:

class Solution {
public:
    int sumSubarrayMins(vector<int>& arr) {
        int n = arr.size();
        vector<int> monoStack;
        vector<int> left(n), right(n);
        for (int i = 0; i < n; i++) {
            while (!monoStack.empty() && arr[i] <= arr[monoStack.back()]) {
                monoStack.pop_back();
            }
            left[i] = i - (monoStack.empty() ? -1 : monoStack.back());
            monoStack.emplace_back(i);
        }
        monoStack.clear();
        for (int i = n - 1; i >= 0; i--) {
            while (!monoStack.empty() && arr[i] < arr[monoStack.back()]) {
                monoStack.pop_back();
            }
            right[i] = (monoStack.empty() ? n : monoStack.back()) - i;
            monoStack.emplace_back(i);
        }
        long long ans = 0;
        long long mod = 1e9 + 7;
        for (int i = 0; i < n; i++) {
            ans = (ans + (long long)left[i] * right[i] * arr[i]) % mod; 
        }
        return ans;
    }
};

执行用时:76 ms, 在所有 C++ 提交中击败了68.14%的用户
内存消耗:41.2 MB, 在所有 C++ 提交中击败了63.99%的用户
复杂度分析
时间复杂度:O(n),其中 n 为数组的长度。利用单调栈求出每个元素为最小值的子序列长度需要的时间为 O(n),求出连续子数组的最小值的总和需要的时间为 O(n),因此总的时间复杂度为 O(n)。
空间复杂度:O(n)。其中 n 为数组的长度。我们需要保存以每个元素为最小元素的子序列长度,所需的空间为 O(n)。
author:LeetCode-Solution

方法二:动态规划

  设 s[j][i] 表示子数组 [arr[j], arr[j+1], ⋯, arr[i]] 的最小值,则可以推出所有连续子数组的最小值之和为 3
对于每个以 i 为最右的子数组最小值之和为
4

我们只需要求出以每个元素 arr[i] 为最右的子数组最小值之和,即可求出所有的子数组的最小值之和。每当我们减少 j 时,子序列的最小值可能会有关联,事实上我们可以观察到 s[j−1][i] = min(s[j][i],arr[j−1])。
  假设当前数组为: arr=[1,7,5,2,4,3,9],当 i = 5 时,所有以索引 j 为起点且以 i 结尾的连续子序列为:

【907. 子数组的最小值之和】_第1张图片
  上述序列的最小值分别为 [3, 3, 2, 2, 2, 1],可以发现重要点是 j = 5, j = 3, j = 0,分别是 j 从 i 开始向左移动遇到的最小值的位置。如下图所示:
【907. 子数组的最小值之和】_第2张图片
设以 arr[i] 为最右且最小的最长子序列长度为 k:

  • 当 j >= i - k + 1 时:连续子序列 [arr[j],arr[j+1],⋯,arr[i]] 的最小值为 arr[i],即 s[j][i] = arr[i]。

  • 当 j < i - k + 1 时:连续子序列 [arr[j],arr[j+1],⋯,arr[i]] 的最小值一定比 arr[i] 更小,通过分析可以知道它的最小值 s[j][i] = min(s[j][i−k],arr[i]) = s[j][i−k]。

则可以知道递推公式如下:

【907. 子数组的最小值之和】_第3张图片
  我们维护一个单调栈,很容易求出元素 x 的左边第一个比它小的元素,即求出以 x 为最右且最小的子序列的最大长度,子数组的最小值之和即为
8

具体解法过程如下:

  • 从左向右遍历数组并维护一个单调递增的栈,如果栈顶的元素大于等于当前元素 arr[i] 则弹出栈,此时栈顶的元素即为左边第一个小于小于当前值的元素;

  • 我们求出以当前值为最右且最小的子序列的长度 k,根据上述递推公式求出 dp[i],最终的返回值即为
    9

代码:

class Solution {
public:
    int sumSubarrayMins(vector<int>& arr) {
        int n = arr.size();
        long long ans = 0;
        long long mod = 1e9 + 7;
        stack<int> monoStack;
        vector<int> dp(n);
        for (int i = 0; i < n; i++) {
            while (!monoStack.empty() && arr[monoStack.top()] > arr[i]) {
                monoStack.pop();
            }
            int k = monoStack.empty() ? (i + 1) : (i - monoStack.top());
            dp[i] = k * arr[i] + (monoStack.empty() ? 0 : dp[i - k]);
            ans = (ans + dp[i]) % mod;
            monoStack.emplace(i);
        }
        return ans;
    }
};

执行用时:68 ms, 在所有 C++ 提交中击败了85.68%的用户
内存消耗:39.8 MB,在所有 C++ 提交中击败了76.70%的用户
复杂度分析
时间复杂度:O(n),其中 n 为数组的长度。利用单调栈求出每个元素为最小值的子序列长度需要的时间为 O(n)。
空间复杂度:O(n)。其中 n 为数组的长度。需要存储每个元素为结尾的子序列最小值之和,所需的空间为 O(n)。
author:LeetCode-Solution

你可能感兴趣的:(LeetCode,算法,数据结构,排序算法,c++,leetcode)