给定一个整数数组 arr,找到 min(b) 的总和,其中 b 的范围为 arr 的每个(连续)子数组。
由于答案可能很大,因此 返回答案模 10^9 + 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]。
最小值为 3,1,2,4,1,1,2,1,1,1,和为 17。
示例2:
输入:arr = [11,81,94,43,3]
输出:444
思路: 考虑所有满足 A[j] 为最右且最小的元素的子序列个数 #(j),那么结果就是 sum #(j) * A[j]。(我们必须考虑最右这样才可以构造互不相交的子序列,否则会出现多次计算,因为一个数组的最小值可能不唯一。)
这就变成考虑最小的下标 i <= j 满足 A[i], A[i+1], …, A[j] 都 >= A[j] 以及最大的下标 k >= j 满足 A[j+1], A[j+2], …, A[k] 都 > A[j]。
算法
例如,A = [10, 3, 4, 5, 3, 2, 3, 10] 我们需要计算 #(j = 4),也就是第二个数字 3 ,被标记的那个,我们会发现 i = 1 和 k = 5。
由此,实际的总数为 #(j) = (j - i + 1) * (k - j + 1) 其中 j - i + 1 选择子序列的左端点 i, i+1, …, j,而 k - j + 1 选择子序列的右端点 j, j+1, …, k。
对于每个询问(也就是根据 j 计算出 (i, k))是一个经典问题,可以用一个栈来解决。我们会重点解答如何找到 i,而找到 k 的做法与之类似。
构造前序数组
做法是维护一个栈,A 的单调下降子序列(事实上是维护一个 A 的下标索引)。对于下一个询问,候选界限为 i* - 1,其中 A[i*] 以递增顺序存储。
现在考虑升序下标 j ,我们可以按照 i* 递减顺序移走所有 A[i*] <= A[j]。
例如,假设 A = [10, 5, 3, 7, 0, 4, 5, 2, 1, 8] 那么当考虑 j = 9 (A[j] = 8),我们有一个存储边界的栈类似于 [-1, 0, 3, 6](代表 A[i*] = -inf, 10, 7, 5)。我们从栈中弹出 6 和 3 因为 5 <= 8 且 7 <= 8,因此得到询问的边界为 i* - 1 = 0。
这个过程线性的,因为只进行了线性次的入栈和出栈。
代码:
import java.util.ArrayDeque;
import java.util.Deque;
public class Solution {
public int sumSubarrayMins(int[] A) {
int MOD = 1_000_000_007;
int N = A.length;
// 第 1 步:计算当前下标 i 的左边第 1 个比 A[i] 小的元素的下标
Deque<Integer> stack1 = new ArrayDeque<>();
int[] prev = new int[N];
for (int i = 0; i < N; i++) {
while (!stack1.isEmpty() && A[i] <= A[stack1.peekLast()]) {
stack1.removeLast();
}
prev[i] = stack1.isEmpty() ? -1 : stack1.peekLast();
stack1.addLast(i);
}
// 第 2 步:计算当前下标 i 的右边第 1 个比 A[i] 小的元素的下标
Deque<Integer> stack2 = new ArrayDeque<>();
int[] next = new int[N];
for (int i = N - 1; i >= 0; i--) {
while (!stack2.isEmpty() && A[i] < A[stack2.peekLast()]) {
stack2.removeLast();
}
next[i] = stack2.isEmpty() ? N : stack2.peekLast();
stack2.addLast(i);
}
// 第 3 步:计算结果
long ans = 0;
for (int i = 0; i < N; ++i) {
// 注意:乘法可能越界,须要先转成 long 类型
ans += (long) (i - prev[i]) * (next[i] - i) % MOD * A[i] % MOD;
ans %= MOD;
}
return (int) ans;
}
}