[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 }
相关题目