[亚麻高频题] Leetcode 907. Sum of Subarray Minimums(子数组最小值之和)

题目描述&链接

Leetcode 907 : 给定一个数组,找到所有子数组最小值,并返回所有最小值之和

题目思路

1. 暴力枚举

直观思路就是通过暴力枚举出所有的子数组,并求所有子数组最小值之和,因为题目子数组是指连续子数组,所有暴力枚举就是枚举起始点和终止点,两层for loop。然后遍历的过程中打擂台保存当前最小值,并求和。直接上代码:

 public int sumSubarrayMins(int[] arr) {
        // 暴力求解起点终点
        int res = 0;
        
        // 枚举出所有范围
        for(int st=0; st<arr.length; st++) {
            int min = arr[st];
            
            
            for(int ed=st; ed<arr.length; ed++) {
                if(arr[ed]<min) min = arr[ed];
                res = (res+min)%100_000_0007;
            }
        }
        
        return res;
        
    }

暴力枚举需要枚举所有起始点和终止点,时间复杂度:O(N^{2});空间复杂度:O(1)。 

2. 逆向思维 + 单调栈

这道题思路与Leetcode 828比较类似,我们正向思维会想办法查找出所有的子数组然后求他的最小值,那么能不能换一种思路,我们找到一个值求解这个值能作为最小值作用的范围。

举例:有一个数组[3,1,2,4],我去找每一个点作为最小值作用的范围

Array : [3, 1, 2, 4]

当前值为3,左边前一个<3的位置-1(没有),右边下一个<3的位置1
- 起始点属于(-1, 0],终止点属于[0, 1),只有子数组为[3]时最小值为3 => (0-(-1))*(1-0) = 1种情况

当前值为1,左边前一个<1的位置-1(没有),右边下一个<1的位置4(没有)
- 起始点属于(-1, 1]任选一个,终止点属于[1, 4)任选一个,子数组为最小值为1 => (1-(-1))*(4-1) = 6种情况

当前值为2,左边前一个<2的位置1,右边下一个<2的位置4(没有)
- 起始点属于(1, 2]只能选2,终止点属于[2, 4)任选一个,子数组最小值为2 => (2-1)*(4-2) = 2种情况


当前值为4,左边前一个<4的位置2,右边下一个<4的位置4(没有)
- 起始点属于(2, 3],终止点属于[3, 4),子数组为最小值为4 => (3-2)*(4-3) = 1种情况

总共子数组个数: 1+6+2+1 = 10种情况
- 验证数组长度为4的子数组个数:
    子数组长度=1(4种) + 
    子数组长度=2([3,1],[1,2],[2,4]3种情况) + 
    子数组长度=3(两种) + 
    子数组长度=4(1种) 
    = 10种

所有只要知道了每一个元素X前一个<X和后一个<X的位置,我们就可以在O(N)时间内完成求和。那么如何求解一个元素前一个比他小和后一个比他小的位置呢,这正是单调栈解决的问题。

  • 单调栈: 是指一个栈内保存的元素满足单调递增或者递减的关系
  • 单调栈 - 概念模板题目:Leetcode 496. Next Greater Element
  • 单调栈最典型的应用就是解决这两种问询: 
    • 求当前元素前一个大于或者小于他的值位置
    • 求当前元素下一个大于或者小于他的值位置

通过单调栈如果记录下一个小于当前值的位置,具体思路维护一个单调递增栈:遍历数组,如果下一个元素比当前stack中元素大那么加入stack中,如果比栈顶小那么,将stack中所有大于该值的元素pop出来,该值就是pop出来的这些元素的下一个小于这些元素的位置,代码如下:

int n = arr.length;

int[] nextSmall = new int[n];
Arrays.fill(nextSmall, n); // 如果没有下一个更小值那么默认值arr.length
Stack<Integer> stk = new Stack<>();
stk.push(0); // 将第一个元素位置压入栈

for(int i=1; i<arr.length; i++) {
    while(!stk.isEmpty() && arr[i]<arr[stk.peek()]) {
        // 如果出现小于栈顶元素,stack pop
        int idx = stk.pop();
        nextSmall[idx] = i;
    }
    stk.push(i);
}

同理求解前一个小于当前值位置,从后向前遍历求解即可。

  • 当数组中出现重复元素  

之前的例子没有重复元素,我们需要考虑如果有重复元素,那么会不会引入重复子数组,下面这个例子可以看出,我们应该寻找的是前一个小于等于当前值的位置,从而避免引入等于当前值位置而造成重复解问题。

加入数组 [3, 1, 4, 1, 2]

3作最小值范围 (0-(-1))*(1-0) = 1种

index=1上的1作为最小值范围 (1-(-1))*(5-1) = 8种

4作最小值范围 (2-1)*(3-2) = 1种

index=3上的1作为最小值范围 (3-(-1))*(5-3) = 8种
- !!!这里就出现了问题,这里面起始点包含了index=1处的1导致有重复解,所以选取起始点时应该上一个1之前的点不应该选

2作为最小值范围 (4-3)*(5-4) = 1种

这样这道题整体思路也就比较明朗了,代码如下:

class Solution {
    public int sumSubarrayMins(int[] arr) {
        // 遍历数组,找到每一个位置前面和后面比他小元素的位置
        int n = arr.length;
        int[] lmin = new int[n];
        Arrays.fill(lmin, -1);
        int[] rmin = new int[n];
        Arrays.fill(rmin, n);
        
        Stack<Integer> stk1 = new Stack<>();
        Stack<Integer> stk2 = new Stack<>();
        stk1.push(0); // add first element
        stk2.push(arr.length-1);
        
        // 前后更小的元素,单调递增栈
        for(int i=1; i<arr.length; i++) {
            // 如果递增加入递减就是找到下一个比他小的
            while(!stk1.isEmpty() && arr[i]<arr[stk1.peek()]) {
                int idx = stk1.pop();
                rmin[idx] = i;
            }
            stk1.push(i);
        }
        
        for(int i=arr.length-2; i>=0; i--) {
            // 如果递增加入,如果小就找到前面一个比他小的值
            while(!stk2.isEmpty() && arr[i]<=arr[stk2.peek()]) {
                int idx = stk2.pop();
                lmin[idx] = i;
            }
            stk2.push(i);
        }
        
        // 然后寻找每一个元素作为最小值的范围 (i-l)*(r-i)*val
        long res = 0;
        int mod = 1000_000_007;
        
        for(int i=0; i<arr.length; i++) {
            int l = lmin[i];
            int r = rmin[i];
            long sum = (long)(i-l)*(r-i)*arr[i];
            
            res = (res + sum)%mod;
        }
        
        return (int)res;
    }
}

时间复杂度:O(N);空间复杂度:O(N), 开辟O(N)来保存单调栈和前后小于当前值位置。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值