【leetcode】子数组的最小值之和

单调栈在解决算法问题时是一个很优化的思路,可以降低时间复杂度。

在接雨水问题——动态规划+单调栈,学习了一道比较经典的单调栈问题,接下来,仍然是对单调栈的一个练习。

1. 题目描述

题目链接:907. 子数组的最小值之和

【leetcode】子数组的最小值之和_第1张图片

2. 思路分析

这题意思是,遍历所有的连续子数组,然后求所有子数组中最小值之和。

1)暴力法

遍历所有区间,然后对于每个区间找出最小值求和。这种方法时间复杂度是 O(n^3) ,显然不可行。

2)暴力法优化

对于区间左端点 i ,遍历所有的右端点 j ,然后维护最小值,时间复杂度可以降到 O(n^2) ,但还是太高了。

3)单调栈

既然我们不能先遍历区间,然后找最小值,那么我们不如顺序倒过来,对于每个值,我们找有多少区间里面,它是最小值。

对于一个数字 A[i] 来说,如果在某个区间 [j, k] 里面它是最小值,那么 [j, k] 包含 A[i] 的子数组的最小值也一定是 A[i] 。所以我们只需要找出最大的那个区间,使得 A[i] 是最小值就行了。

A = [3, 1, 2],A[1] = 1,在0到2里面都是最小值,那么[0, 2]里包含A[1]的子数组最小值也一定是A[1]。

[3, 1], [1], [3, 1, 2], [1, 2] 最小值都是A[1]。

另一个性质是,左右端点 j 和 k 是相互独立的,不会影响,因为 [i, k] 的改变并不会改变 [j, i] 的最小值。所以我们只需要分别求出 A[i] 往左和往右的最远距离就行了。

因为往左和往右求解方法是类似的,所以我们只需要看一个方向就行了。同样不能遍历一遍,不然就和暴力法没区别了嘛。这时候就要介绍神器了——单调栈

单调栈是一个栈,后进先出,里面的元素是单调递增或递减的。而在这题里面,我们要求的是 A[i] 左边最远的距离,等价于求左边第一个比它小的数字 A[j] 。而 A[j+1], …, A[i] 都大于等于 A[i] ,所以都可以作为符合要求区间的左端点。

这里单调栈只需要维护一个单调上升的子序列就行了,遍历到一个数 A[i] 的时候,如果栈顶的元素大于等于 A[i] ,那么就出栈,直到第一个小于 A[i] 的数 A[j] 为止,那么 A[i] 为最小值的区间左端点可选择数量为 j - i。为什么这样是对的呢?因为 A[j] 是栈里面第一个小于 A[i] 的数,而 A[j] 后面的数都大于 A[j] ,这样才不会把 A[j] 顶出栈。而如果栈是空的,就说明 A[i] 前面的所有元素都大于等于它,那么所有区间都符合条件了。

而右边最大的范围同理可以求得,但是这里有个需要注意的地方!如果存在两个相同的数,这么算不是会导致同一个区间在两个数的位置处计算两次吗?所以要稍稍改进一下,既然向左计算的时候,已经包含了相等的值了,那么向右计算就要排除掉了。也就是从右往左计算右边最远范围的时候,只能计算右边第一个小于等于它的位置,而向左是计算第一个小于它的位置。这样就不会重复计算了。

4)单调栈+动态规划

上面的方法不仅要考虑两端的范围,还得考虑去重,真是麻烦又容易写错。下面介绍一种更加好写又不容易写错的方法,只是不那么容易想到。

我们定义dp[i] 为所有以 i 为右端点的区间的最小值之和,同样用单调栈的方法来寻找左边最远的距离,使得区间内 A[i] 是最小值。假设用单调栈找到了左边第一个比 A[i] 小的数是 A[j] ,那么 dp[i] 就可以加上 (i - j) * A[i] ,因为 A[j] 往右都是 A[i] 最小。而 A[j] 再往左呢?这些区间最小值等价于直接以 A[j] 为右端点的最小值,因为 A[j] 往右的数都比它大,没有影响,所以 dp[i] 再加上 dp[j] 就行了。

上面两种方法时间复杂度都是 O(n) 的,因为进栈出栈最多也只需要 2n 次。

3. Java实现

首先,对于单调栈,用LinkedList来存储数组下标,来维护单调递增栈。

LinkedList<Integer> stack = new LinkedList<>();  // 单调栈,存储元素下标

如果栈不空,且当前数组小于栈顶元素,则出栈。

while (!stack.isEmpty() && arr[i] < arr[stack.peek()]) {
    stack.pop();  // 栈不空,且新元素小于栈顶元素,则出栈
}

然后,对于动态规划,dp[i]表示以i为右端点的所有子数组的和。

int[] dp = new int[n];  // 表示以i 为右端点的所有子数组的和

这里有两种情况:

  1. 当栈空,即当前元素小于栈顶元素,则新加的所有子数组的最小值都是arr[i],共i + 1个。

    if (stack.isEmpty()) {
        dp[i] = (i + 1) * arr[i];
    }
    
  2. 当栈不空,即当前元素大于栈顶元素,则所有子数组(除不含当前元素)的最小值还是栈顶元素,不含当前元素的数组的最小值,即当前元素下标与栈顶元素下标的差×当前元素。

    else {
        dp[i] = dp[stack.peek()] + (i - (stack.peek())) * arr[i];
    }
    

可以从动态规划再来解释以下为什么需要单调栈:

假设遍历到当前位置i,那么dp[i]表示【0...i】 【1...i】【i-2...i】【i-1,i】所有包括i的连续数组的和。那么当nums[i+1]大于前面所有值时,dp[i+1]可以由dp[i]+nums[i+1]快速得出。假如nums[i+1]这个位置的值大于nums[j](j >= 0 && j < i),那么就可以把包括i+1的连续数组分成两部分,右边这部分的最小值nums[i+1],左边这部分自然由dp[j]得出。这也是需要一个单调递增栈的原因。

class Solution {
    public int sumSubarrayMins(int[] arr) {
        long res = 0;
        int n = arr.length;
        long mod = 1000000007;
        LinkedList<Integer> stack = new LinkedList<>();  // 单调栈,存储元素下标
        int[] dp = new int[n];  // 表示以i 为右端点的所有子数组的和
        for (int i = 0; i < n; i++) {
            // 维护单调栈
            while (!stack.isEmpty() && arr[i] < arr[stack.peek()]) {
                stack.pop();  // 栈不空,且新元素小于栈顶元素,则出栈
            }
            if (stack.isEmpty()) {
                dp[i] = (i + 1) * arr[i];
            } else {
                dp[i] = dp[stack.peek()] + (i - (stack.peek())) * arr[i];
            }
            stack.push(i);
            res += dp[i];
        }
        return (int)(res % mod);
    }
}

执行用时:16 ms, 在所有 Java 提交中击败了82.35% 的用户。

单调栈优化:用数组表示单调栈。

class Solution {
    public int sumSubarrayMins(int[] arr) {
        // 单调栈+动态规划
        int n = arr.length;
        long res = 0;
        int[] dp = new int[n + 1]; // 子区间最小值之和
        int[] stack = new int[n + 1];
        int top = 0;
        int mod = 1000000007;
        // 初始化
        Arrays.fill(stack, - 1);
        dp[0] = 0;
        for (int i = 0; i < n; i++) {
            // 构建单调栈
            while (top > 0 && arr[i] <= arr[stack[top]]) {
                top--;
            }
            dp[i + 1] = (dp[stack[top] + 1] + (i - stack[top]) * arr[i]) % mod;
            stack[++top] = i; // 入栈
            res += dp[i + 1];
        }
        return (int)(res % mod);
    }
}

执行用时:6 ms, 在所有 Java 提交中击败了95.71% 的用户。

你可能感兴趣的:(算法分析,算法,单调栈,动态规划)