给定正数数组arr,求任意子数组最小值与子数组累加和的乘积最大值是多少

给定正数数组arr,求任意子数组最小值与子数组累加和的乘积最大值是多少?

提示:单调栈使用的经典题目

咱们学过俩重要的单调栈的基础知识:

之前咱们刚刚学过单调栈1:
(1)单调栈1:o(1)复杂度求数组arr在窗口w内的最大值或者最小值
(2)单调栈2:寻找数组arr中i位置左边距离i最近的比i小的位置L,右边距离i最近的比i小的位置R

咱们今天就用这里的单调栈解决问题


题目

给定正数数组arr,求任意子数组最小值min与子数组累加和sum的乘积的最大值是多少?
不妨设子数组为L–R范围上的数组
累加和sum,窗口内min知道
求sum×min的最大值是多少?


一、审题

示例:
arr=1 1 1 1 1
不妨设子数组为L–R范围上的数组,
由于数组arr都是1,任意子数组的最小值min=1
minsum=sum,则找sum最大值就行,由于arr大于0,所以L–R长度越长越好,即N长度=5最好
这样的话,min
sum的最大值就是1*5=5
在这里插入图片描述
这要是arr不是全1呢
比如arr=1 2 3 3 2 1


暴力解,不可,o(n^2)复杂度

比如arr=1 2 3 3 2 1

咱们可以这样做,让每一个arri都做一次最小值min,看看它能向左,向右扩展多大范围,形成的子数组,累加和求上来,再求乘积
每一个i遍历之后,必然最大值就在其中

(0)这样的话,外围o(n)调度
(1)内部,每次i,向左搜索最左边最近的比i小的那个位置L,向右搜索右边最近的比i小的那个位置R,图中累加和放入sum
因为只有找到左边L,右边R,中间L–R范围内,i才能做最小值!!
(2)计算sum*[i],更新给max结果–需要o(n)速度
(3)最后返回max即可

整体复杂度就是o(n^2),显然者不可取!!!

单调栈解决,将其优化到o(n)复杂度

既然你暴力解都想到了宏观调度,这个想法很好,就让每个i做一次最小值,看看往左右能扩多大范围子数组L–R
因为arr大于零,这个扩的越远,越好,sum越大

但是最重要的优化:求累加和这个事,可以o(1)速度搞定的,

咱们老早就学过了,因为arr大于零
求arr任意范围上的L–R的累加和sum,是可以用前缀累加和数组来加速的
也就是前缀累加和数组pre,是arr从0–N-1不断地累加得到的前缀数组。【下图pre】
因为有了前缀累加和数组pre,则L–R累加和就是:sum[L–R] = pre[R]-pre[L-1]
在这里插入图片描述
好,只要咱们来到arr的i位置,只要知道了L,R,则再以o(1)速度拿到L–R的累加和,岂不是很爽?整个过程就o(1)

L,R上面说了,arr中距离i左边最近的比i小的位置L,距离i右边最近的比i小的位置R
这敏感度你需要有:(2)单调栈2:寻找数组arr中i位置左边距离i最近的比i小的位置L,右边距离i最近的比i小的位置R

看到了吗,你储备的知识,单调栈可以用了,它就是让你o(1)速度拿到i处左右的最近比i小的L和R位置的。
这速度够快吧!这基础知识,是不是很重要?所以继续往下看之前,你必须学会上面的基础知识(2)文章

okay,总结一波!
解决本题的代码逻辑流程:
(0)arr准备累加和数组pre
arr外围o(n)调度遍历每一个位置i,将[i]拿来做最小值,求得的max=[i]×sum[L–R]
(1)内部,每次i,用单调栈,o(1)拿到:
向左搜索最左边最近的比i小的那个位置L,向右搜索右边最近的比i小的那个位置R,
然后再o(1)速度拿到L–R上的累加和放入sum【注意L,R位置的那俩数不要哦】
(2)计算sum*[i],更新给max结果–这就需要o(1)速度了哦
(3)最后返回max即可
在这里插入图片描述
整体就被优化为o(n)了!爽吧

有了单调栈的代码,咱直接它来求本题题目的结果!!!
单调栈的代码,返回的是pos数组,每个i的左右LR

//复习:
    //真正的单调栈,肯定要考虑数组arr是重复的情况,所以咱们来手撕代码实现上述流程吧!
    public static int[][] getLeftRightNearestLessiPosition(int[] arr){
        if (arr == null || arr.length == 0) return null;

        int N = arr.length;
        int[][] ans = new int[N][2];//结果0L,1R
        //用栈搞定
        Stack<List<Integer>> stack = new Stack<>();

        //(0)遍历每一个位置i,方便收集答案
        for (int i = 0; i < N; i++) {
            //(1)一上来,先看看需要弹出栈顶收集答案吗?因为每次我们面对的i都可能收集答案,除了第一次i=0
            while (!stack.isEmpty() && arr[i] < arr[stack.peek().get(0)]){
                // 需要的话,看栈空吗?空:L=-1,否则L就是此时新栈顶pNew的末尾位置那个i
                //他们的R是此刻的i;
                List<Integer> list = stack.pop();//先弹出来
                int R = i;
                int L = stack.isEmpty() ? - 1 : stack.peek().get(stack.size() - 1);//新栈顶的末尾位置
                for(Integer j:list){
                    ans[j][0] = L;
                    ans[j][1] = R;
                }
            }

            //(2)(1)走完,咱们就要压栈i进去
            //注意,如果栈顶已经存在了,添加即可,否则要新建list
            if (!stack.isEmpty() && arr[i] == stack.peek().get(0)){
                //有了
                stack.peek().add(i);
            }else {
                //没有的话
                List<Integer> list = new ArrayList<>();
                list.add(i);
                stack.push(list);
            }
        }
        //(3)如果i=N了那越界出来了,还要处理栈中剩下的元素们,结算他们,
        // 他们所有的R=-1;如果最新的栈顶pNew不空,则L就是pNew的末尾位置那个i,空的话L=-1;
        while (!stack.isEmpty()){
            // 需要的话,看栈空吗?空:L=-1,否则L就是此时新栈顶pNew的末尾位置那个i
            //他们的R是此刻的i;
            List<Integer> list = stack.pop();//先弹出来
            int R = -1;
            int L = stack.isEmpty() ? - 1 : stack.peek().get(stack.size() - 1);//新栈顶的末尾位置
            for(Integer j:list){
                ans[j][0] = L;
                ans[j][1] = R;
            }
        }

        return ans;
    }

本题的解题代码:
再看看宏观调度:
(0)arr准备累加和数组pre
arr外围o(n)调度遍历每一个位置i,将[i]拿来做最小值,求得的max=[i]×sum[L–R]
(1)内部,每次i,用单调栈,o(1)拿到:
向左搜索最左边最近的比i小的那个位置L,向右搜索右边最近的比i小的那个位置R,
然后再o(1)速度拿到L–R上的累加和放入sum
(2)计算sum*[i],更新给max结果–这就需要o(1)速度了哦【L,R处的数不要】
(3)最后返回max即可
在这里插入图片描述

手撕代码,中间有个小逻辑要搞清楚,不容易!!!

//给定正数数组arr,求任意子数组最小值与子数组累加和的乘积最大值是多少
    //再看看宏观调度:
    public static int maxMultiplyOfMinAndSubSum(int[] arr){
        if (arr == null || arr.length == 0) return 0;
        //(0)arr准备累加和数组pre
        int N = arr.length;
        int[] pre = new int[N];//0位置当没有
        pre[0] = arr[0];//默认
        for (int i = 1; i < N; i++) {
            pre[i] = pre[i - 1] + arr[i];//累加上来
        }
        //L--R累加和就变为pre[R]-pre[L-1]
        int max = Integer.MIN_VALUE;//ans
        //向左搜索最左边最近的比i小的那个位置L,向右搜索右边最近的比i小的那个位置R
        int[][] pos = getLeftRightNearestLessiPosition(arr);

        //arr外围o(n)调度遍历每一个位置i,将[i]拿来做最小值,求得的max=[i]×sum[L--R]
        //	(1)内部,每次i,用单调栈,o(1)拿到:
        for (int i = 0; i < N; i++) {
            //	向左搜索最左边最近的比i小的那个位置L,向右搜索右边最近的比i小的那个位置R,
            int L = pos[i][0] == -1 ? 0 : pos[i][0];
            int R = pos[i][1] == -1 ? N - 1 : pos[i][1];//为了方便待会求累加和的,左边-1换成0,右边-1换为N
            //	然后再o(1)速度拿到L--R上的累加和放入sum
            int sum = 0;
            if (pos[i][0] == -1) {//左边L是-1的话,0--R-1就是sum
                sum = pos[i][1] != -1 ? pre[R - 1] : pre[R];//正常L--R的L,R不能算哦,R=N时要N-1位置
            }else {
                //L不是-1的话,左边的L不能要
                sum = pos[i][1] != -1 ? pre[R - 1] - pre[L]: pre[R] - pre[L];//正常L--R的L,R不能算哦,R=N时要N-1位置
            }
            //如果R==N,arr[i]找补回来
            //	(2)计算sum*[i],更新给max结果--这就需要o(1)速度了哦
            max = Math.max(max, arr[i] * sum);
        }

        //(3)最后返回max即可
        return max;
    }


    public static void test(){
        int[] arr = {1,2,5,2,1};
        //1:1*1==1
        //2:2*2=4
        //3:3*(3+3)=9
        //4:4*4=16
        //5:5*(5+5)=50

        System.out.println(findTarget(arr));
        System.out.println(maxMultiplyOfMinAndSubSum(arr));
    }

    public static void main(String[] args) {
        test();
    }

里面的

int L = pos[i][0] == -1 ? 0 : pos[i][0];
int R = pos[i][1] == -1 ? N - 1 : pos[i][1];//为了方便待会求累加和的,左边-1换成0,右边-1换为N

是这样的,pre因为没有-1位置,
所以L=-1的话,你要换为0,但是求sum时,咱们要算上arr[0],正常真的是L=0时,求sum时arr[L]不能要的
同样的,R=-1时,咱们要换位N-1,但是sum要算上arr[N-1]位置那个数,正常真的是R=N-1时,求sum时arr[N-1]不能要的
在这里插入图片描述
如果一般情况下,L和R找到了,我们求累加和是不能要L和R位置的数的。(就像上图粉色那个括号的sum)
如果pos[i][0] = -1,则L=0,此时我们要arr[0],如果此刻,pos[i][1] != -1,说明R不能要,则取绿色那个括号为sum
如果pos[i][1] != -1,说明arr[N-1]得要,就像橘色那个括号代表的sum

如果pos[i][0] != -1,我们不能要L,如果此刻,pos[i][1] != -1,说明R不能要,则取粉色那个括号为sum
如果pos[i][1] != -1,说明arr[N-1]得要,就像蓝色那个括号代表的sum

这也就是为啥咱们要大费周章控制好下标的原因:

//	然后再o(1)速度拿到L--R上的累加和放入sum
            int sum = 0;
            if (pos[i][0] == -1) {//左边L是-1的话,0--R-1就是sum
                sum = pos[i][1] != -1 ? pre[R - 1] : pre[R];//正常L--R的L,R不能算哦,R=N时要N-1位置
            }else {
                //L不是-1的话,左边的L不能要
                sum = pos[i][1] != -1 ? pre[R - 1] - pre[L]: pre[R] - pre[L];//正常L--R的L,R不能算哦,R=N时要N-1位置
            }

好好理解这个下标的事情,然后本题测试:

25
25

理解了解题思路,抠代码就好办了,自己要花点时间coding清楚。


总结

提示:重要经验:

1)单调栈的敏感度:求滑动窗口内的最大值,最小值,或者,求i左边右边距离i最近比i小的位置L,R
2)前缀累加和数组pre的敏感度,要随时可以想到,尤其是arr大于0时,这个可以加速算法。
3)笔试求AC,可以不考虑空间复杂度,但是面试既要考虑时间复杂度最优,也要考虑空间复杂度最优。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冰露可乐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值