• [LeetCode] 907. Sum of Subarray Minimums


    Given an array of integers arr, find the sum of min(b), where b ranges over every (contiguous) subarray of arr. Since the answer may be large, return the answer modulo 109 + 7.

    Example 1:

    Input: arr = [3,1,2,4]
    Output: 17
    Explanation: 
    Subarrays are [3], [1], [2], [4], [3,1], [1,2], [2,4], [3,1,2], [1,2,4], [3,1,2,4]. 
    Minimums are 3, 1, 2, 4, 1, 1, 2, 1, 1, 1.
    Sum is 17.
    

    Example 2:

    Input: arr = [11,81,94,43,3]
    Output: 444

    Constraints:

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

    子数组的最小值之和。

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

    由于答案可能很大,因此 返回答案模 10^9 + 7 。

    来源:力扣(LeetCode)
    链接:https://leetcode.cn/problems/sum-of-subarray-minimums
    著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

    题目给的是数组,要我们求的东西也很简单,就是找出 input 数组里所有的子数组,并且把每个子数组里的最小值拿出来,计算所有最小值的和。

    暴力解不难想,可以手动找出所有的子数组,然后找每个子数组的最小值。但是介于题目的数据范围会到 10 的 4 次方,暴力解会超时。我直接介绍最优解单调栈。我参考了这个帖子

    单调栈的特点就是可以在 O(n) 的时间内找到数组里的一个拐点。如下代码,是一个单调递增栈的代码。当我们遍历到 A[i] 的时候,如果 A[i] 小于栈顶元素,那么我们就一直把栈顶元素弹出。弹出操作完毕之后,再放入 A[i]。这个操作我们很熟悉,但是对于这道题,单调栈巧妙的地方在于当我们从 while 循环跳出来之后,也就正好找到了包含 A[i] 的子数组的左边界,且 A[i] 是这个子数组的最小值。

    1 for (int i = 0; i < A.size(); i++) {
    2     while(!in_stk.empty() && in_stk.top() > A[i]) {
    3         in_stk.pop();
    4     }
    5     in_stk.push(A[i]);
    6 }
    • 为什么找到了左边界?因为栈弹出元素的规则就是如果栈顶元素更大就弹出,且从栈里弹出的元素一定在 A[i] 的左侧,是先于 A[i] 被放入栈内的。所以如果 A[i] 是某一段子数组的最小值,那么他的左边界可以一直包含所有从栈中被弹出的元素
    • 为什么 A[i] 是这一段子数组的最小值?因为他一直比栈顶元素小,直到遇到了一个比 A[i] 更小的元素。停下的位置就是左边界的位置。

    我们通过这个方式可以找到所有包含 A[i] 且 A[i] 是最小值的子数组的左边界,我们同时也要找到右边界,才能确定整个子数组的长度。所以我们需要创建两个和 input 数组等长的数组,一个记录每个数字 A[i] 可以辐射到的左边界 left[i],一个记录可以辐射到的右边界 right[i]。这个辐射范围的定义是以 A[i] 为最小值的子数组的左边界和右边界。最后结算的时候,对于每个数字 A[i],以 A[i] 为最小值可以形成的子数组的数量 = i 到左边界的距离 * i 到右边界的距离。

    最后需要提醒的是,每个数字 A[i] 都有可能是某个非空子数组的最小值,比如长度为 1 的子数组,只包含 A[i] 自身的子数组,A[i] 自然就是这个子数组的最小值。单调栈的做法真的很巧妙。

    时间O(n)

    空间O(n)

    Java实现

     1 class Solution {
     2     private int MOD = (int) Math.pow(10, 9) + 7;
     3 
     4     public int sumSubarrayMins(int[] arr) {
     5         // corner case
     6         if (arr == null || arr.length == 0) {
     7             return 0;
     8         }
     9 
    10         // normal case
    11         int len = arr.length;
    12         int[] left = new int[len];
    13         int[] right = new int[len];
    14         Deque<Integer> stack = new ArrayDeque<>();
    15         for (int i = 0; i < len; i++) {
    16             while (!stack.isEmpty() && arr[i] < arr[stack.peek()]) {
    17                 stack.pop();
    18             }
    19             if (stack.isEmpty()) {
    20                 left[i] = -1;
    21             } else {
    22                 left[i] = stack.peek();
    23             }
    24             stack.push(i);
    25         }
    26 
    27         stack.clear();
    28         for (int i = len - 1; i >= 0; i--) {
    29             // 需要有一边是带等号的,才不会漏解
    30             while (!stack.isEmpty() && arr[i] <= arr[stack.peek()]) {
    31                 stack.pop();
    32             }
    33             if (stack.isEmpty()) {
    34                 right[i] = len;
    35             } else {
    36                 right[i] = stack.peek();
    37             }
    38             stack.push(i);
    39         }
    40 
    41         long res = 0;
    42         for (int i = 0; i < len; i++) {
    43             res = (res + (long) (i - left[i]) * (right[i] - i) * arr[i]) % MOD;
    44         }
    45         return (int) res;
    46     }
    47 }

    这里我再提供一个只需要扫描一遍的做法,思路也是单调栈。刚才第一种做法,单调栈为什么扫描两次是因为对于每一个 A[i] ,我们第一次找到 A[i] 能cover到的左边界,第二次找到 A[i] 能cover到的右边界。现在我们只需要扫描一遍就可以做到。

    注意一下扫描一遍的时候我们会得到什么信息。我们会将每个大于当前元素 A[i] 的元素出栈以向左求解得到第一个小于 A[i] 的元素,那么反过来对于每个出栈的元素,当前元素 A[i] 不就是向右比它更小的第一个元素吗?这就得到了被弹出元素的右边界。

    每个大于 A[i] 的元素都会出栈,那么每个能入栈的元素在栈内相邻的那个元素,不就是刚刚出栈的那个元素的左边界吗?

    时间O(n)

    空间O(n)

    Java实现

     1 class Solution {
     2     public int sumSubarrayMins(int[] arr) {
     3         int MOD = (int) Math.pow(10, 9) + 7;
     4         int len = arr.length;
     5         long sum = 0;
     6         Deque<Integer> stack = new ArrayDeque<>();
     7         int j;
     8         int k;
     9         for (int i = 0; i <= len; i++) {
    10             int cur = i == len ? Integer.MIN_VALUE : arr[i];
    11             while (!stack.isEmpty() && cur < arr[stack.peek()]) {
    12                 j = stack.pop();
    13                 k = stack.isEmpty() ? -1 : stack.peek();
    14                 sum += (long) arr[j] * (i - j) * (j - k);
    15             }
    16             stack.push(i);
    17         }
    18         return (int) (sum % (long) MOD);
    19     }
    20 }

    相关题目

    907. Sum of Subarray Minimums

    2104. Sum of Subarray Ranges

    LeetCode 题目总结

  • 相关阅读:
    二维数组实现01背包
    一维数组实现01背包
    js实现最长子串算法
    mysql__存储过程
    mysql 分页查询 limit
    转:严重: Exception loading sessions from persistent storage
    struts2---ValueStack对象
    struts2----ognl
    什么是JavaBean?
    EL表达式
  • 原文地址:https://www.cnblogs.com/cnoodle/p/16463255.html
Copyright © 2020-2023  润新知