首页 > 试题广场 >

子数组的最小值之和

[编程题]子数组的最小值之和
  • 热度指数:746 时间限制:C/C++ 1秒,其他语言2秒 空间限制:C/C++ 256M,其他语言512M
  • 算法知识视频讲解
给定一个长度为 n 的数组 nums ,算出他所有的 min(sub) 之和,其中 sub 指数组 nums 的所有连续子数组 , min(sub) 指子数组的最小值。

数据范围: ,数组中的值满足 ,由于结果可能非常大,所以返回结果对 取模的结果
示例1

输入

[3,1,2,4,5]

输出

30

说明

子数组有 3、1、2、4、5、(3,1)、(3,2)、(3,4)、(3,5)、(1,2)、(1,4)、(1,5)、(2,4)、(2,5)、(4,5)、(3,1,2)、(3,1,4)、(3,1,5)、(3,2,4)、(3,4,5)、(1,2,4)、(1,2,5)、(2,4,5)、(3,1,2,4)、(3,1,2,5)、(3,2,4,5)、(1,2,4,5) 其最小值和是 30 
这道题如果直接双层循环暴力求解,显然O(N^2) 时间复杂度,效率非常低,如下给出单调栈的解法:
1.  构建一个单调递增栈,只有当前元素大于栈顶元素时才加入
2.  如果当前元素比栈顶元素小,则说明当前元素最小,开始对栈中的每个比当前元素大的元素操作
3.  对于每个弹出的元素,设为 idx, 它栈中的上一个元素,设为 idx_pre, 那么对于 idx_pre ~ idx 来说,这些里面的元素肯定都大于等于 idx, 因为如果小于 idx, 那一定是已经加入栈中了,因此左边范围 (idx_pre, idx) 之间的元素,最小值为 idx对应的值,因此左边比 idx 大的值的范围是  idx - idx_pre, 其中的 idx_pre = stack.peek()
4.  再看右边,初次循环时,i - idx =1, 此时right =1, 如果还有比 i 对应元素大的值,则继续循环,当到达某次循环时 , idx 到 i 之间的元素肯定比当前的idx 要大,因为假如有比 idx 要小的元素,那么它要入栈,在入栈时肯定在 i 之前就把 idx 弹出来了,根本不会等到第 i 个元素才弹出,因此右边比 idx 大的元素的范围是 i - idx
5. 因此一个单调递增的数组,是不会触发弹栈的,也就不会对栈中的元素进行计算连续数组的最小贡献值,除非遇到一个较小的元素,如果最后一个元素还是递增的,那么我们就可以假设第 n 个元素是满足条件的,此时 i = n, 触发所有栈中剩余的元素进行处理。 从而不会遗漏子数组
6. 综上,当以 arr[idx] 为最小元素时,所有连续数组的个能个数就是 left * right, 他们的最小值之和就是  arr[idx] * left * right
7. 把上述过程转换为代码,如下:

public int sumSubarr (ArrayList<Integer> nums) {
        int[] arr = nums.stream().mapToInt(Integer::valueOf).toArray();
        // write code here
        int n = arr.length;
        long res = 0;
        int mod = 1000000007;
        Stack<Integer> stack = new Stack<>();

        for (int i = 0; i <= n; i++) {
            // 当发现较小的元素时
            while (!stack.isEmpty() && (i == n || arr[stack.peek()] > arr[i])) {
                int idx = stack.pop(); // 弹出并处理
                // idx 左边的元素, peek 到 idx 肯定都比 idx 大,否则不会被 pop 掉
                int left = idx - (stack.isEmpty() ? -1 : stack.peek());
                int right = i - idx;
                res += (long) arr[idx] * left * right;
                res %= mod;
            }
            stack.push(i);
        }
        return (int) res;
    }
问: arr[i] 是 left 和 right + 1范围内的最小值,为何不用它计算最终结果
答:i 会入栈,后续弹栈会处理到它
总结:较难理解


发表于 2024-05-14 12:22:01 回复(0)
# -*- coding: utf-8 -*-

#
# 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
#
#
# @param nums int整型一维数组
# @return int整型
#
class Solution:
    """
    题目:
        https://www.nowcoder.com/practice/a7401d0dd4ec4071a31fd434e150bcc2?tpId=196&tqId=40517&rp=1&ru=/exam/oj&qru=/exam/oj&sourceUrl=%2Fexam%2Foj%3FjudgeStatus%3D3%26page%3D1%26pageSize%3D50%26search%3D%26tab%3D%25E7%25AE%2597%25E6%25B3%2595%25E7%25AF%2587%26topicId%3D196&difficulty=undefined&judgeStatus=3&tags=&title=
    参考:
        https://leetcode.cn/problems/sum-of-subarray-minimums/solution/zi-shu-zu-de-zui-xiao-zhi-zhi-he-by-leetcode/
    算法:
        对于每个 j,考虑所有子序列 [i, j] 的最小值。想法是每当我们增加 j,这些最小值可能会有关联,事实上,min(A[i:j+1]) = min(A[i:j], A[j+1])。
        我们维护最小值栈stack,stack中存储的元素为(x, c),表示以x为结尾且x为最小值的区间个数,stack按照x单调递增。枚举区间右端点j,弹出栈中 x >= y的元素,因为这些以x结尾且以x为最小值的区间,尾部追加y之后,变成以y结尾且以y为最小值的区间了。另外我们使用Sum来记录以当前元素y结尾的所有区间的最小值之和。
    复杂度:
        时间复杂度:O(n)
        空间复杂度:O(n)
    """

    def sumSubarr(self, nums):
        # write code here
        MOD = 10 ** 9 + 7
        ans, Sum, stack = 0, 0, []

        for j, y in enumerate(nums):
            count = 1
            while stack and stack[-1][0] >= y:
                x, c = stack.pop() # 以元素x结尾的区间个数为c,现在我们将元素y作为这些区间的尾部,也就是之前c个以元素x为最小值的区间 变为 以元素y作为最小值的区间
                count += c
                Sum -= x * c # dot始终记录以当前元素y结尾的所有区间的最小值之和
            stack.append((y, count)) # count表示以元素y结尾且y作为区间最小值的区间个数
            Sum += y * count
            ans += Sum # 累加以y结尾的所有区间的最小值之和

        return ans % MOD

if __name__ == "__main__":
    sol = Solution()

    # nums = [3, 1, 2, 4, 5]

    # nums = [3, 1, 2, 4]

    # nums = [11, 81, 94, 43, 3]

    nums = [1, 2]

    # nums = [3, 2, 1]

    res = sol.sumSubarr(nums)

    print res

发表于 2022-07-08 19:30:40 回复(0)
import java.util.*;

public class Solution {
    public int sumSubarr (ArrayList<Integer> nums) {
        // 枚举nums[j]为最小数:向左向右找第一个更小的

        Deque<Integer> stack = new ArrayDeque<>();
        stack.push(-1); // 左侧更小的默认下标-1
        long res = 0, n = nums.size();
        for (int k = 0; k <= n; k++) {
            int x = k < n ? nums.get(k) : -1; // 右侧更小的默认下标n
            while (stack.size() > 1 && nums.get(stack.peek()) > x) {
                int j = stack.pop();
                int i = stack.peek();
                res += 1L * nums.get(j) * (k - j) * (j - i); // 包含nums[j]的子数组
                res %= 1000_000_007;
            }
            stack.push(k);
        }
        return (int)res;
    }
}

发表于 2025-10-16 13:51:17 回复(0)
import java.util.*;
import java.util.stream.IntStream;

/**
 * NC386 子数组的最小值之和
 * @author d3y1
 */
public class Solution {
    private final int MOD = 1000000007;

    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     *
     * @param nums int整型一维数组
     * @return int整型
     */
    public int sumSubarr (ArrayList<Integer> nums) {
        int[] iNums = nums.stream().mapToInt(Integer::intValue).toArray();
        // int[] iNums = nums.stream().mapToInt(i->i).toArray();
        return solution1(iNums);
        // return solution2(iNums);
    }

    /**
     * 动态规划 + 单调栈
     *
     * 举例子找规律:
     * nums数组
     * 0  1  2  3  4  5  6  7  8  9  10
     * 23 43 18 16 35 23 6  11 6  33 30
     *
     * leftMap(i)  表示当前元素nums[i]左边第一个小于当前元素的元素索引 -1:表示没找到(当前元素左边都比当前元素大)
     * rightMap(i) 表示当前元素nums[i]右边第一个小于当前元素的元素索引  N:表示没找到(当前元素右边都比当前元素大)
     *      i       0  1  2  3  4  5  6  7  8  9  10
     * leftMap(i)  -1  0 -1 -1  3  3 -1  6 -1  8  8
     * rightMap(i)  2  2  3  6  5  6  11 8  11 10 11
     *
     * dp[i]表示以元素nums[i]为开头的所有连续子数组的最小值的和
     *    i    0  1  2  3  4  5  6  7  8  9  10
     * dp[0]   23 23 18 16 16 16 6  6  6  6  6
     * dp[1]      43 18 16 16 16 6  6  6  6  6
     * dp[2]         18 16 16 16 6  6  6  6  6
     * dp[3]            16 16 16 6  6  6  6  6
     * dp[4]               35 23 6  6  6  6  6
     * dp[5]                  23 6  6  6  6  6
     * dp[6]                     6  6  6  6  6
     * dp[7]                        11 6  6  6
     * dp[8]                           6  6  6
     * dp[9]                              33 30
     * dp[10]                                30
     *
     * dp[i] = nums[i]*(rightMap.get(i)-i)+dp[rightMap.get(i)]
     * 
     * @param nums
     * @return
     */
    private int solution1(int[] nums){
        int N = nums.length;

        Map<Integer, Integer> rightMap = new HashMap<>();
        Stack<Integer> rightStack = new Stack<>();
        int num;
        // 右边 降序
        for (int i=N-1; i>=0; i--){
            // 找到当前元素右边第一个小于当前元素的元素索引 N:表示没找到, 当前元素右边都比当前元素大
            num = nums[i];
            // 单调增
            while (!rightStack.isEmpty() && num<=nums[rightStack.peek()]){
                rightStack.pop();
            }
            rightMap.put(i, rightStack.isEmpty() ? N : rightStack.peek());
            rightStack.push(i);
        }

        int sum = 0;
        int[] dp = new int[N+1];
        for(int i=N-1; i>=0; i--){
            dp[i] = nums[i]*(rightMap.get(i)-i)+dp[rightMap.get(i)];
            sum = (sum+dp[i]) % MOD;
        }
        
        return sum;
    }

    /**
     * 动态规划 + 单调栈
     *
     * 举例子找规律:
     * nums数组
     * 0  1  2  3  4  5  6  7  8  9  10
     * 23 43 18 16 35 23 6  11 6  33 30
     *
     * leftMap(i)  表示当前元素nums[i]左边第一个小于当前元素的元素索引 -1:表示没找到(当前元素左边都比当前元素大)
     * rightMap(i) 表示当前元素nums[i]右边第一个小于当前元素的元素索引  N:表示没找到(当前元素右边都比当前元素大)
     *      i       0  1  2  3  4  5  6  7  8  9  10
     * leftMap(i)  -1  0 -1 -1  3  3 -1  6 -1  8  8
     * rightMap(i)  2  2  3  6  5  6  11 8  11 10 11
     *
     * dp[i]表示以元素nums[i]为结尾的所有连续子数组的最小值的和
     *    i    0  1  2  3  4  5  6  7  8  9  10
     * dp[0]   23
     * dp[1]   23 43
     * dp[2]   18 18 18
     * dp[3]   16 16 16 16
     * dp[4]   16 16 16 16 35
     * dp[5]   16 16 16 16 23 23
     * dp[6]   6  6  6  6  6  6  6
     * dp[7]   6  6  6  6  6  6  6  11
     * dp[8]   6  6  6  6  6  6  6   6  6
     * dp[9]   6  6  6  6  6  6  6   6  6 33
     * dp[10]  6  6  6  6  6  6  6   6  6 30 30
     *     
     *         { nums[i]*(i-leftMap.get(i))                       , leftMap.get(i) == -1
     * dp[i] = {
     *         { nums[i]*(i-leftMap.get(i)) + dp[leftMap.get(i)]  , leftMap.get(i) != -1
     *
     * @param nums
     * @return
     */
    private int solution2(int[] nums){
        int N = nums.length;

        Map<Integer, Integer> leftMap = new HashMap<Integer, Integer>();
        Stack<Integer> leftStack = new Stack<>();
        int num;
        // 左边 升序
        for (int i=0; i<N; i++){
            // 找到当前元素左边第一个小于当前元素的元素索引 -1:表示没找到, 当前元素左边都比当前元素大
            num = nums[i];
            // 单调增
            while (!leftStack.isEmpty() && num<=nums[leftStack.peek()]){
                leftStack.pop();
            }
            leftMap.put(i, leftStack.isEmpty() ? -1 : leftStack.peek());
            leftStack.push(i);
        }

        int sum = 0;
        int[] dp = new int[N];
        for(int i=0; i<N; i++){
            dp[i] = nums[i]*(i-leftMap.get(i));
            if(leftMap.get(i) != -1){
                dp[i] += dp[leftMap.get(i)];
            }
            sum = (sum+dp[i]) % MOD;
        }
        
        return sum;
    }
}

发表于 2025-02-12 12:34:18 回复(0)