[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

2281. Sum of Total Strength of Wizards

LeetCode 题目总结

posted @ 2022-07-10 15:26  CNoodle  阅读(255)  评论(0编辑  收藏  举报