在问 · 算法题解

907. 子数组的最小值之和

中等动态规划单调栈 + 动态规划(以 i 结尾的子数组最小值之和)力扣原题 ↗

题目描述

输入整数数组 arr。

考虑 arr 的每一个非空连续子数组,取出它的最小值,再把所有这些最小值加起来。

结果可能很大,返回总和对 10^9 + 7 取模后的值。

示例 1
输入:arr = [3,1,2,4]
输出:17

解释:10 个子数组的最小值依次为 3、1、2、4、1、1、2、1、1、1,和为 17。

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

数据范围

  • 1 <= arr.length <= 3 * 10^4
  • 1 <= arr[i] <= 3 * 10^4

解题思路:单调栈 + 动态规划(以 i 结尾的子数组最小值之和)

设 dp[i] 为所有以 i 结尾的子数组的最小值之和。找到 i 左边第一个比 arr[i] 小的位置 j:起点在 j+1..i 的 i-j 个子数组最小值都是 arr[i];起点在 j 及更左的子数组,最小值和以 j 结尾的对应子数组一样,和为 dp[j]。所以 dp[i] = dp[j] + (i - j) · arr[i]。“左边第一个更小的数”用单调递增栈求。答案为 Σ dp[i]。

为什么正确

处理完 i 后,栈中下标对应的值严格递增,栈顶是 i;弹掉的下标都不可能再成为后面位置的“左边第一个更小数”。

复杂度分析

时间 O(n)(每个下标最多入栈、出栈各一次),空间 O(n)。

易错点

出栈条件用 >=(相等也弹出),让 j 是“严格更小”的位置。相等元素此时会被当前的 arr[i] 统一计入,不重不漏。C++/Java 的 (i-j)·arr[i] 要用 long,并及时取模。

参考代码

Python、Java、C++、Go、JavaScript、TypeScript 六种语言,逻辑一致。

class Solution:
    def sumSubarrayMins(self, arr: List[int]) -> int:
        MOD = 10 ** 9 + 7
        n = len(arr)
        dp = [0] * n
        stack = []
        ans = 0
        for i in range(n):
            while stack and arr[stack[-1]] >= arr[i]:
                stack.pop()
            j = stack[-1] if stack else -1
            dp[i] = (dp[j] if j >= 0 else 0) + (i - j) * arr[i]
            stack.append(i)
            ans += dp[i]
        return ans % MOD

载入编辑器,自己跑一遍 →

其他解法

主解法按右端点做 DP,一遍单调栈即可;贡献法按最小值元素分组计数,需要左右边界,但思路可以直接套用到更多题目。

解法 2:贡献法(单调栈求左右边界) 时间 O(n),空间 O(n)

换个角度:对每个 arr[i],统计它是多少个子数组的最小值。用单调栈求出左边第一个严格更小的位置 L 和右边第一个小于等于它的位置 R,左端点有 i - L 种选法、右端点有 R - i 种选法,贡献就是 arr[i] × (i - L) × (R - i)。一边严格、一边不严格,是为了让有重复最小值的子数组只算一次。

“枚举每个元素做最小值的范围”是一类题的通用套路(如子数组范围和、柱状图中最大的矩形),迁移性强。但重复元素的边界一侧取严格、一侧取非严格,写错就会重复计数或漏计;还要两个边界数组。

class Solution:
    def sumSubarrayMins(self, arr: List[int]) -> int:
        MOD = 10 ** 9 + 7
        n = len(arr)
        left = [-1] * n
        right = [n] * n
        stack = []
        for i in range(n):
            while stack and arr[stack[-1]] >= arr[i]:
                right[stack.pop()] = i
            left[i] = stack[-1] if stack else -1
            stack.append(i)
        ans = 0
        for i in range(n):
            ans += arr[i] * (i - left[i]) * (right[i] - i)
        return ans % MOD

动态规划 专题的其他题目

查看 动态规划 全部题目 →