持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第12天,点击查看活动详情
今天的每日一题是907. 子数组的最小值之和 - 力扣(LeetCode),这道题其实可以利用暴力搜索的方式进行求解,但是时间复杂度很高。因此可以采用动态规划 + 单调栈的方法进行求解。
题目内容
给定一个整数数组 arr
,找到 min(b)
的总和,其中 b
的范围为 arr
的每个(连续)子数组。
由于答案可能很大,因此 返回答案模 10^9 + 7
。
样例
输入: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。
输入: arr = [11,81,94,43,3]
输出: 444
方法
从题目可知需要我们先获得所有的子数组,并对每个子数组的最小值进求和,其实一种最简单的方法就是利用搜索找到所有的子数组,并求出每个子数组的最小值,累加得到最终的和。这个过程虽然可以得到答案,但是时间复杂度过高。并且由于结果的数值很大,不方便对其进行存储。
因此可以反过来思考这个问题,原题目中需要我们去找每个字数的最小值,那么可不可以在遍历数组时,对当前当前元素属于哪些子数组的最小值进行筛选。
假设用dp[i]
表示到达位置0
到位置i
的范围里子数组的最小和,而以数组元素arr[i]
为最右且最小的最长子序列长度为 k
。
这说明
arr[i]
可以作为i - k
到k
范围里包含arr[i]
元素子数组的最小值。
这样可以得到状态转移方程:dp[i] = dp[i - k] + k * arr[i]
dp[i]
:位置0
到位置i
的范围里子数组的最小和dp[i - k]
:位置0
到位置i - k
的范围里子数组的最小和k * arr[i]
:i - k + 1
到i
范围内的子数组的最小值之和
对于k
的计算,就是要获得arr[i]
的左边第一个小于该元素的位置。对于求解当前元素左边第一个小于的问题,可以采用单调栈的方式来解决。
具体代码如下:
public int sumSubarrayMins(int[] arr) {
int n = arr.length;
long ans = 0;
final int MOD = 1000000007;
// 单调栈
Deque<Integer> monoStack = new ArrayDeque<>();
int[] dp = new int[n];
for (int i = 0; i < n; i++){
// 如果栈顶的元素大于当前元素则弹出,保证栈中存储的值是一定小于当前的元素
while (!monoStack.isEmpty() && arr[monoStack.peek()] > arr[i]){
monoStack.pop();
}
// 根据单调栈找到左边第一个比他小的
int k = monoStack.isEmpty() ? (i + 1) : (i - monoStack.peek());
dp[i] = k * arr[i] + (monoStack.isEmpty() ? 0 : dp[i - k]);
ans = (ans + dp[i]) % MOD;
monoStack.push(i);
}
return (int)ans;
}