求数组arr中达标的子数组的数量:子数组内最大值与最小值之差小于等于target

该博客详细解析了一道关于寻找数组中满足特定条件子数组数量的题目,利用单调栈将时间复杂度从O(n^3)优化到O(n)。通过分析示例和解释单调栈的基础知识,阐述了如何在窗口内快速求解最大值和最小值,以及如何在R扩展过程中保持单调性,避免不必要的重复计算,从而提高算法效率。
摘要由CSDN通过智能技术生成

求数组arr中达标的子数组的数量:子数组内最大值与最小值之差小于等于target

提示:经典的单调栈的应用题目
极度重要的基础知识点:单调栈:
(1)单调栈:o(1)复杂度求数组arr在窗口w内的最大值或者最小值

想要做出本题来,必须理解上面的单调栈基础知识(1)。
想要做出本题来,必须理解上面的单调栈基础知识(1)。
想要做出本题来,必须理解上面的单调栈基础知识(1)。


题目

给定数组arr,给定整数target,如果arr中任意子数组内最大值与最小值之差小于等于target,则称之为达标子数组,请你求arr中达标的子数组的数量。


一、审题

示例:arr=3 2 1 4
target=1
咱们举例完全可以用暴力解枚举,也启发咱们的宏观调度
ans=0收集结果;
(1)不妨设让3做开头,则看看以i=0开头的子数组,连续有几个达标的?
下图可见,3 可以达标,3 2可以打标,但是3 2 1不达标,所以往后以3开头的子数组全部不可能达标!,因为子数组连续的
ans +=2
在这里插入图片描述
(2)让i=1做开头,2 可以达标,2 1达标,2 14 不达标,往后也不可能达标了
ans = 2+2=4
在这里插入图片描述
(3)让i=2开头试试有几个?1 可以达标,1 4 不可能
ans = 4+1=5
在这里插入图片描述
(4)以i=3开头的有几个?4 可以达标
ans = 5+1=6
所以答案就6个!


暴力解:不可

上面示例中,咱们就用暴力解搞定了一下

以每一个i位置为头,看看它能往右使劲扩展多少个子数组出来?
累加起来就是总的数量

外围的宏观调度就需要o(n)
中间以i开头刷一遍子数组,需要o(n)
每一个子数组,找max和min,又需要o(n)才能判断是否达标?
这样的话暴力解的复杂度o(n^3)肯定不行的

在这里插入图片描述

理解单调栈如何将o(n^2)优化到o(1)的

看看暴力解就知道,外围的以每次L开头,要去求一个结果n,这个宏观调度是躲不了的,外围的**o(n)**就是必然的
结果就是ans=n1+n2+……+nn;
在这里插入图片描述
咱们想办法在求内部一个子数组达标这件事上,看看能不能把o(n^2)的复杂度,优化到o(n)乃至o(1)??

根据(1)单调栈:o(1)复杂度求数组arr在窗口w内的最大值或者最小值

咱们有可能找到突破口!

事情是这样的:我们不妨设窗口内子数组范围是[L–R),左闭右开,之前单调栈求过一个窗口内的max和min是o(1)速度就可以拿到
有了单调栈,就可以把内部的o(n^2)的复杂度,优化到o(n)

而以L开头,R不断往右扩,达标,就让R++,一旦不达标,R截止。
这个o(n)的扩充过程,可以优化到o(1)吗?【事实是可以!

真的需要每一次,都从L那开始遍历找min和max去判断子数组L–R是否达标吗?
在这里插入图片描述
不需要!

本题的特点就在这,示例中咱们也看到了,当L开头的子数组,往右扩,只要在第一个R不达标的地方截止,往后R再扩,没用了,都不会达标
比如下图:L=0开头,L–R=0可以,L–R=1可以,L–R=2可以,L–R=3 可以,那从此往后,以L=0开头的子数组,R往后扩都不会达标!
在这里插入图片描述
那么刚刚扩到第一个R=3处不达标,L–R中有多少个达标呢?R-L个全部达标
因为前L–R递增过程中,每一个子数组你都达标,咱们才扩R的。
第一个R=3不达标,从此往后不再扩,也不会达标的。
因此R实际上扩了几次达标呢?R=0,1,2,3次,恰好也是R-L

好,现在L++,咱们看下一个L开头的情况:
从现在L开始,R真的需要回到L这个地方,从头去判断各个L–R都达标吗?

不需要!
不需要!
不需要!

为什么??因为上一个L–R达标的话,意味着内部任意子数组都达标的【max-min<=target】
如果L–R达标,则必然L+1–R也达标,如果L+1–R不满足这个条件,你这个R早就不能扩了。看下图:
在这里插入图片描述
图中L–R是达标的,那L+1–R必然达标
现在L=L+1了,你不需要把R整回到L重新判断一遍了,此时的L–R天然达标的,因为上一轮【L+1–R必然达标】给你做过了达标判断。
现在L=L+1时,你只需直接看当前的L–R是否达标即可!!!【上一轮R是不包括的】

!这是不是非常非常巧妙
!这就是单调栈带来的好处,具备单调性。R不用重新回来搞一遍,
所以中间L–R这个遍历子数组的o(n)的过程,咱们优化到o(1)了

外围L从0开头,1开头……N-1开头这个o(n)的宏观调度避免不了。
内部的o(n^2)【找max和min与遍历L–R同时】被优化到o(1)

所以咱们用单调栈来解决这个问题,也就总体**o(n)**的速度!!这可比暴力解优化了太太多了吧!!!


单调栈解决本题的代码逻辑

OK,总结一下,咱们怎么做这个题,写代码呢?
利用L–R左闭右开,表示窗口内达标的子数组
准备好max和min的单调栈:双向队列,他们的求法代码都是同一套逻辑【看基础知识那个文章!】。

(0)外围宏观调度,让每一个L做一次开头,统计有每个L开头时,有多少个子数组达标?这个数量,累加到ans做结果
(1)内部呢,每一次L开头,L–R内用单调栈找max和min,o(1)速度拿到他们俩,判断max-min<=target吗?
是,继续扩R++,否则,R就是第一次性不达标的R,从此往后不必再扩R了,统计ans+=R-L
(2)(1)不能扩了,那换开头L再统计,L++;新的L开头,也不需要R再回到L重新刷一遍,只需要去(1)继续判断就行。
中间一定要注意,L–R作为一个窗口,窗长自然是不固定的,所以R++时,不需要检查L是否过期,咱们一定要收集L–R内的max和min
知道R第一个不达标,咱们再收集答案,判断L是否过期,如何判断L过期?是单调栈内部的重要知识【看基础知识那个文章!】

整体只剩外围o(n)复杂度,是不是超级超级牛?

手撕代码

//复习
    public static int validNumOfArr(int[] arr, int target){
        if (arr == null || arr.length == 0) return 0;
        int N = arr.length;
        //利用L--R左闭右开,表示窗口内达标的子数组
        //准备好max和min的单调栈:双向队列,他们的求法代码都是同一套逻辑【看基础知识那个文章!】
        int L = 0;//左闭
        int R = 0;//右开
        int ans = 0;//结果统计数量

        LinkedList<Integer> queueMax = new LinkedList<>();
        LinkedList<Integer> queueMin = new LinkedList<>();
        // (0)**外围宏观调度**,让每一个L做一次开头,统计有每个L开头时,
        // 有多少个子数组达标?这个数量,累加到ans做结果
        while (L < N){
            //	(1)内部呢,每一次L开头,L--R内用单调栈找max和min,o(1)速度拿到他们俩,判断max-min<=target吗?
            //	是,继续扩R++,否则,R就是第一次性不达标的R,从此往后不必再扩R了,统计ans+=R-L
            while (R < N){//达标继续扩
                //达标与否,需要拿到max和min,这需要单调栈调度
                while (!queueMax.isEmpty() && arr[R] >= arr[queueMax.peekLast()])
                    queueMax.pollLast();//扩R,就要让R进队列,大逼小离开
                //然后大的进来
                queueMax.addLast(R);
                while (!queueMin.isEmpty() && arr[R] <= arr[queueMin.peekLast()])
                    queueMin.pollLast();//扩R,就要让R进队列,小逼大离开--跟上面逻辑类似
                //然后大的进来
                queueMin.addLast(R);

                //判断达标才能继续扩,否则结束
                int max = arr[queueMax.peekFirst()];
                int min = arr[queueMin.peekFirst()];
                if (max - min > target) break;//不达标,结束R东扩

                //一直到第一个R不能扩
                R++;
            }
            //然后扩不动了就要统计结果!
            ans += R - L;

            //对于单调栈的调度,扩不动R自后就要核验队头是否过期?
            //w不是固定长度哦,所以不是要在R扩时,检查窗长w,而R不能扩时再检查
            if (L == queueMax.peekFirst()) queueMax.pollFirst();//弹出头,让其过期
            if (L == queueMin.peekFirst()) queueMin.pollFirst();//弹出头,让其过期

            //	(2)(1)不能扩了,那换开头L再统计,L++;
            //	新的L开头,**也不需要R再回到L重新刷一遍**,只需要去(1)继续判断就行。
            //换头需要L++,换头也不需要R重新回来,继续判断就行
            L++;
        }

        return ans;
    }


    public static void test(){
        int[] arr = {3,2,1,4};
        int target = 1;
        System.out.println(getNum(arr, target));
        System.out.println(validNumOfArr(arr, target));
    }

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

这个题,是不是很经典地运用了单调栈,求窗口内最大最小值
关键是,咱们在R++时,保持了单调性,统计结果时不需要R回退到L来重新求一遍——这个知识点是数组三连问题中,咱们就遇到过的知识点。要慢慢熟悉这种单调性带来的好处。这可以舍去那些不必要求的过程,加速算法!

但凡遇到这种舍弃的思想,题目挺难的,但是优化好了,能大大降低你的算法复杂度!!!
但凡遇到这种舍弃的思想,题目挺难的,但是优化好了,能大大降低你的算法复杂度!!!
但凡遇到这种舍弃的思想,题目挺难的,但是优化好了,能大大降低你的算法复杂度!!!


总结

提示:重要经验:

1)今后遇到求子数组窗口内的最大值,最小值,直接敏锐地敏感地想到要用单调栈!
2)如果求结果的过程中,发现双指针R++后,有一定的单调性,就不需要R回退了,这可以舍去那些不必要求的过程,加速算法!
3)但凡遇到这种舍弃的思想,题目挺难的,但是优化好了,能大大降低你的算法复杂度!!!
4)笔试求AC,可以不考虑空间复杂度,但是面试既要考虑时间复杂度最优,也要考虑空间复杂度最优。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

冰露可乐

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

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

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

打赏作者

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

抵扣说明:

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

余额充值