907-子数组的最小值之和


1. 题面

给定一个整数数组 arr,找到 min(b) 的总和,其中 b 的范围为 arr 的每个(连续)子数组。

由于答案可能很大,因此 返回答案模 10^9 + 7 。

示例 1:

输入: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。
示例 2:

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

提示:

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

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/sum-of-subarray-minimums
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

2. 解题分析

首先对题意进行理解,题目给的示例1的含义是:

通过穷举出题目所给出的nums所延伸出的所有子数组,然后找出这些子数组中的最小值,然后把他们加起来,这样就能够求出来答案。

那么这样的暴力解法的时间复杂度是多少呢?

首先对于暴力回溯求所有子数组,就相当于求子集的问题,最坏情况下会去到2^n的时间复杂度,然后每个子数组都遍历一次求min,最坏情况下会去到n的时间复杂度,最终就是O(2^n*n)的时间复杂度,一看题目给的length,必然超时。

回过头来考虑这个问题的话,可以这样想,对于每一个nums[i]所在的区间,nums[i]都有可能作为一个最小值而存在,那么什么时候是最小值呢?如果我们能够找到一个区间j∈[left,right],其中,所有的nums[i]<=nums[j],这时候nums[i]就可以作为一个最小值。

注:这里这样做是会导致重复元组的出现的,因此需要限定一下左右边界的大小。

对于左边界,我们允许nums[i]<=nums[j], 但是对于右边界,我们只 允许nums[i]<nums[j]

那么一个简单的思路就是从nums[i]开始,从两边出发,找到一个符合条件的[left,right],最终结算即可。

结算的公式:

//先来考虑只有如果只有一边的边界,这时候会如何
//假定nums[left]是在nums[i]左边,第一个比nums[i]要小的数的下标
int score1 = (i-left)*nums[i];
//假定nums[right]是在nums[i]右边,第一个比nums[i]要小的数的下标
int socre2 = (right-i)*nums[i];
//对于边界而言
//如果左边没有更小的数了,那么
int left =  -1;
//对于右边界而言
//如果右边没有更小的数了,那么
int right = n;
//由于最终的子数组是两边延伸后得到的结果
int total = (i-left)*(right-i)*nums[i];

3.单调栈分析

我们知道,如果是两边延伸的办法的话,那么最终的时间复杂度会去到O(n^2),最终测试用例的处理次数会去到8次方,大概率是会超时的,如果能够找到一个每个元素只遍历一次,而又能准确地知道其左右边界的算法,那么这道题就迎刃而解了。

单调栈符合这样的要求。

我们要求每一个nums[i]在栈中,第一个压着的肯定是它左边第一个比它小的数,因此

if(nums[stack.peek()] <= nums[i]){//如果栈顶元素比当前检索到的元素要小,那么就入栈
    stack.push(i);
}

同时,我们希望能够找到它右边第一个比它小的数,因此

if(nums[stack.peek()] > nums[i]){//如果栈顶元素比当前检索到的元素要大,那么就出栈并结算
    int peek = stack.pop();
    socore = (i-stack.peek())*(peek-i)*nums[peek];
}

这个就是单调栈的大致思路,最终累加socre即可

4. AC代码

首先回顾一下单调栈的模板

vector<vector<int>> getNear(vector<int>& nums){
    vector<vector<int>> ans(nums.size());
    stack<vector<int>> s;
    for(int i =0;i<nums.size();i++){
        while (!s.empty() && nums[s.top()[0]]>nums[i]){//如果栈顶元素比当前的检索元素要大,那么就结束它
            //检查左边元素和右边元素,设置答案
            vector<int> top =  s.top();
            s.pop();
            for(int j = 0;j<top.size();j++){//结算该列表中的元素
                int idx = top[j];
                //然后ans[i][0]就是左边离他最近的那个元素,是栈的下边
                //ans[i][1]就是右边离他最近的那个元素
                int left = s.empty()?-1:s.top().back();
                ans[idx].push_back(left);
                ans[idx].push_back(i);
            }
        }
        if(!s.empty() && nums[s.top()[0]] == nums[i]){
            s.top().push_back(i);
        }else{
            vector<int> v;
            v.push_back(i);
            s.push(v);
        }
    }
    while (!s.empty()){
        vector<int> v= s.top();
        s.pop();
        int left = s.empty()?-1:s.top().back();
        for(int i = 0;i<v.size();i++){
            ans[v[i]].push_back(left);
            ans[v[i]].push_back(nums.size());
        }
    }
    return ans;
}
class Solution {
    //求解一个列表,这个列表定义
    //对于nums[i],找到一个nums[left]是第一个比它小的,找到一个nums[right]是第一个比它小的
    //定义arr[i][2]={left,right}
    static final int  mod = (int)(1e9+7);
    public int sumSubarrayMins(int[] nums) {
        int n = nums.length;
        int[][] arr = new int[nums.length][2];
        Stack<List<Integer>> stack = new Stack<>();
        long result = 0;
        //单调栈处理
        for(int i = 0;i < n;i++){
            //其关键是对于左边相等的数要如何进行处理
            while(!stack.isEmpty() && nums[stack.peek().get(0)] > nums[i]){
                //那么就结算当前的peek()
                List<Integer> peek = stack.pop();
                int size = peek.size();
                for(int j = 0;j< size; j++){
                    int left = stack.isEmpty()?-1:stack.peek().get(stack.peek().size()-1);
                    int idx = peek.get(j);
                    if(j == 0){
                        arr[idx][0] = left;//左边这个元素
                    }else{
                        arr[idx][0] = peek.get(j-1);//与前面那个元素相等
                    }
                    arr[idx][1] = i;
                    result = (result + 1L*(arr[idx][1] - idx)*(idx - arr[idx][0])*nums[idx])%mod;
                }
            }
            if(!stack.isEmpty() && nums[stack.peek().get(0)] == nums[i]){
                stack.peek().add(i);
            }else{
                ArrayList<Integer> v = new ArrayList<>();
                v.add(i);
                stack.push(v);
            }
        }
        //如果到最后栈都还没空,那么这时候就要手动清空
        while (!stack.isEmpty()){
            List<Integer> peek = stack.pop();
            int size = peek.size();
            int left = stack.isEmpty()?-1:stack.peek().get(stack.peek().size()-1);
            for(int i = 0;i<size;i++){
                int idx = peek.get(i);
                if(i == 0){
                    arr[idx][0] = left;
                }else{
                    arr[idx][0] = peek.get(i-1);
                }
                arr[idx][1] = nums.length;
                result = (result + 1L*(arr[idx][1] - idx)*(idx - arr[idx][0])*nums[idx])%mod;
            }
        }
        /*
        for(int i = 0;i< nums.length;i++){
            System.out.print(i+":"+nums[i]+" ");
        }
        System.out.println();
        for(int i = 0;i<n;i++){
            System.out.println("i:"+i+",nums[i]="+nums[i]+",left:"+arr[i][0]+",right:"+arr[i][1]);
        }
        //然后结算即可
        System.out.println(result);
        */
        return (int)result;
    }
}

文章作者: 穿山甲
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 穿山甲 !
  目录