【每日一题】:907 子数组的最小值之和

25 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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]。 
最小值为 3124112111,和为 17。

输入: arr = [11,81,94,43,3]
输出: 444

方法

从题目可知需要我们先获得所有的子数组,并对每个子数组最小值进求和,其实一种最简单的方法就是利用搜索找到所有的子数组,并求出每个子数组的最小值,累加得到最终的和。这个过程虽然可以得到答案,但是时间复杂度过高。并且由于结果的数值很大,不方便对其进行存储。

因此可以反过来思考这个问题,原题目中需要我们去找每个字数的最小值,那么可不可以在遍历数组时,对当前当前元素属于哪些子数组的最小值进行筛选。

假设用dp[i]表示到达位置0到位置i的范围里子数组的最小和,而以数组元素arr[i]最右且最小的最长子序列长度为 k

这说明arr[i]可以作为 i - kk范围里包含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 + 1i 范围内的子数组的最小值之和

对于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;
}