数组累加和三连3:arr可小于等于大于0,请问累加和<=k的子数组最大长度是多少

数组累加和三连3:arr可小于等于大于0,请问累加和<=k的子数组最大长度是多少?

提示:之前见过数组累加和的另外三个重要题目,动态规划,可以认为是数组累加和456连

之前的数组累加和三连的第1连:
【1】数组累加和三连1:arr全大于0,请问累加和为k的子数组最大长度是多少
之前数组累加和三连的第2连:
【2】数组累加和三连2:arr可小于等于大于0,请问累加和为k的子数组最大长度是多少

重要的数组累加和456连:
【4】数组arr中必须以i位置结尾的子数组,其最大累加和是多少?
【5】数组arr的0–i范围上任选一个子数组的最大累加和是多少?
【6】给定数组arr和k,求3个不重叠的长为k的子数组的累加和之和最大值是多少?

其中,4为5的基础,5又是6的基础,一定学仔细了

今天要讲得典型数组累加和三连的解法

前面数组三连的12都很简单
今天讲数组累加和三中最难的第3连


题目

数组累加和三连3:arr可小于等于大于0,请问累加和<=k的子数组最大长度是多少
之前的数组1连:是arr全>0,是子数组累加和 =k
之前的数组2连:是arr全<=>0,是子数组累加和 =k
现在的数组3连:是arr全<=>0,是子数组累加和 <=k
知道为啥要1 2 3 连数组累加和了吗!就这么来的。


一、审题

示例:
arr = 1 -1 1 -1 0 1 -1
k=0
<=k=0的子数组累加和最大长度是多少?
可选-1,长度为1
可选1 -1,长度为2
可选1 -1 1 -1,长度为4
可选1 -1 1 -1 0,长度为5
可选1 -1 1 -1 0 1 -1,长度为7
所以这又是没有单调性的事情


暴力解:不可

从i=0–N-1每个位置i
去探索j=i–j一堆子数组,一个个累加,看看是否<=k,一旦>k,将j-i做最大长度,
下一次探索j=i+1–j一堆子数组 ,一个个累加,看看是否<=k,一旦>k,将j-(i+1)做最大长度
……
暴力找吧
外循环N次,内部每次j索引N次
故o(n^2)复杂度
当然不可取!


子数组的问题,往往考虑以i开头或者i结尾的情况

本题难,咱们考虑以i开头,看看i往后能扩多远,整个子数组的累加和还<=k,这长度越长越好。

暴力解怎么解?
每一个i开头,尝试一下?从i–j这么长累加和<=k的长度为多少?j-i+1就是长度
每次i开头,从i–j索引遍历求和……

这未免太复杂!

能不能这样?
用额外空间,换时间?

比如,有一个类似前缀累加和的数组,让我们,尽可能在i位置,便捷地拿到i–j或者更远的累加和
为啥要额外空间信息呢?因为我想别每次都去暴力遍历求和了

这种思想,咱们经常用的!
这种思想,咱们经常用的!
这种思想,咱们经常用的!

但是前缀累加和吧,你是考虑以i开头的情况,你拿到的是0–i上的累加和,咱们想要i–j的累加和呢!
所以前缀累加和不太合适……

因此,咱们换个思路,搞一个后缀累加和?从后往前推的累加和?

反正,我们在i位置,能尽量便捷地知道i–j的累加和,而且还要记录这个j位置,方便咱们求长度j-i+1,更新给max作为结果

这样的话,我从左往右遍历i,在每个i位置,以i开头,秒杀能拿到i–j上的累加和,sum,sum<=k的话,我还能往右多拿点加进来
知道sum>k,说明往后拿太多数了,加入无法满足sum<=k
在这里插入图片描述
比如,上图k=0,
i–j的最小累加和为-3,x–y的最小累加和为3,a–b最小累加和为1,后面的最小累加和为-1

咱们想知道i开头的子数组,累加和sum,能有多长

有了上面那些预设的信息,咱们先把i–j秒杀扩进来,而不是每次都去遍历i–j一个一个求和
(1)发现sum=-3<=k=0,说明,咱们还可以尝试往后扩x–y进来,因为后面更多的加入,也不一定>k
(2)发现sum=-3+3=0<=k=0,说明,咱们还可以尝试往后扩a–b进来,说不定还是没有超标呢?
(3)发现sum=0+1>k了,说明a–b不能扩入,最多能扩到y处,结果先更新一下:y-i+1

你可能想问,a–b扩不进来,单独扩a进来呢?能扩吗,就一个a而已

这里我想说的是,a–b的1是最小累加和,也就是咱们准备额外数组时,这个信息是最小累加和
如果你单独扩a,a一个数,就比a–b的累加和还要大
——我们定义的额外信息就是一串累加和尽量小,缺后面的数字,累加和就会变大

啥是最小累加和呢?就是a使劲往后面扩,能把累加和尽量控制到最小,趋近于0或者趋近于负数,叫最小
比如,可能[a]=2,[a+1]=-1,[0]=0
显然,从a开始往后的子数组,最小的累加和应该是包含a–b的,
因为a单独算累加和是2,加上a+1位置,是1,累加和变更小了,再加b,仍然是1,这样的话,a–b尽可能长但是累加和更小,这就是最小累加和。

故,上面的例子中,你说单独扩a进来,肯定不行,你单独扩2的话,它不是最小的累加和,自然不满足条件,能往后扩,a–b会变更小,那干嘛不把a–b整体扩进来呢。

懂了上面我要干的思想了吗?
我就是想,别每次来到i,都把i–j范围的子数组再遍历求和了!
我想快速根据额外数组,拿到i–j整体上的最小累加和!这样,我方便继续往后扩一个更大的长度,但是sum还<=k
这样求出来的长度,不就是我们要的更理想的,更长的max吗!


给arr准备最小累加和信息数组,这段最小累加和的结尾位置j记住

给你arr
咱们从arr的N-1位置,往前推,把任意i位置开头的子数组,往后扩出来的最小累加和,放入minSum信息数组【i–j范围能扩得又长,累加和又小】
同时,还把这个最小累加和的尾部位置j记录在minSumEnd信息数组中
啥意思?

举例你就明白!
arr = 3 7 4 -6 6 3 -2 0 7 -3 2
位置i:0 1 2 3 4 5 6 7 8 9 10

看看i=5位置开头的子数组,最长最远能累加出最小的累加和是多少?
你单独看5位置的话,sum=3,狗小吗?不
还可以将6位置算进来,sum=3-2=1,最远了吗?不
还可以将7位置算进来,sum=1+0=1,足够远,足够小了吗?是的,j=7就是目前i能扩到最远又小的位置
因为再算8位置进来,累加和就大了,不要8位置
所以以i=5开头的子数组最小累加和是1,将其放入minSum[i],即minSum[i=5]=1
范围是i–j,尾部位置j=7,将其放入minSumEnd[i],即minSumEnd[i=5]=7
在这里插入图片描述
那么今后,我们来到i位置,首先我就知道从i往后扩,能得到最远又最小的累加和是minSum[i=5]=1,而且,我知道范围是i–minSumEnd[i]=j
这不就秒杀知道了吗?

okay,你明白我的良苦用心了吧!用这俩数组,额外空间,换加速时间,秒杀知道了累加和,我就不去i—j一个一个再加一遍了!!!

好,咱们顺势,把整个arr的minSum数组和minSumEnd数组都填好吧
别从前往后填,而是从后往前填

(1)i=10处,目前就一个数,直接用,minSum[10]=arr[i]=2,minSumEnd[10]=i=10位置
(2)i=9处,你会发现,minSum[i+1]=minSum[10]=2>0,咱要把正数扩进来吗?不,扩进来就太大,我们希望扩进来之后,累加和变小,所以10位置,不能扩进来;故:minSum[9]=arr[i]=-3,minSumEnd[9]=i=9位置
在这里插入图片描述
(3)i=8处,你会发现,minSum[i+1]=minSum[8]=-3<=0,既然后面还有负数,或者0,都可以纳入,这样以i开头的子数组才会变得更小,更长,故:minSum[9]=arr[i]+minSum[i+1]=7-3=4,minSumEnd[9]=minSumEnd[i+1]=9位置
在这里插入图片描述
(4)i=7处,你会发现,minSum[i+1]=minSum[8]=4>0,咱要把正数扩进来吗?不,扩进来就太大,我们希望扩进来之后,累加和变小,所以8位置,不能扩进来;故:minSum[7]=arr[i]=0,minSumEnd[7]=i=7位置
在这里插入图片描述
(5)i=6处,你会发现,minSum[i+1]=minSum[7]=0<=0,说明后面还有负数或者0,都可以纳入,这样以i开头的子数组才会变得更小,更长,故:minSum[6]=arr[i]+minSum[i+1]=-2+0=-2,minSumEnd[6]=minSumEnd[i+1]=7位置
在这里插入图片描述
(6)i=5处,你会发现,minSum[i+1]=minSum[6]=-2<=0,说明后面还有负数或者0,都可以纳入,这样以i开头的子数组才会变得更小,更长,故:minSum[5]=arr[i]+minSum[i+1]=3-2=1,minSumEnd[5]=minSumEnd[i+1]=7位置,上图粉色那,看见没
(7)i=4处,你会发现,minSum[i+1]=minSum[5]=1>0,咱要把正数扩进来吗?不,扩进来就太大,我们希望扩进来之后,累加和变小,所以5位置,不能扩进来;故:minSum[4]=arr[4]=6,minSumEnd[4]=i=4位置
在这里插入图片描述
(8)i=3处,你会发现,minSum[i+1]=minSum[4]=6>0,咱要把正数扩进来吗?不,扩进来就太大,我们希望扩进来之后,累加和变小,所以4位置,不能扩进来;故:minSum[3]=arr[3]=-6,minSumEnd[3]=i=3位置
在这里插入图片描述
(9)i=2处,你会发现,minSum[i+1]=minSum[3]=-6<=0,说明后面还有负数或者0,都可以纳入,这样以i开头的子数组才会变得更小,更长,故:minSum[3]=arr[i]+minSum[i+1]=4-6=-2,minSumEnd[2]=minSumEnd[i+1]=3位置
在这里插入图片描述
(10)i=1处,你会发现,minSum[i+1]=minSum[2]=-2<=0,说明后面还有负数或者0,都可以纳入,这样以i开头的子数组才会变得更小,更长,故:minSum[1]=arr[i]+minSum[i+1]=7-2=5,minSumEnd[1]=minSumEnd[i+1]=3位置
在这里插入图片描述
(11)i=0处,你会发现,minSum[i+1]=minSum[1]=5>0,咱要把正数扩进来吗?不,扩进来就太大,我们希望扩进来之后,累加和变小,所以1位置,不能扩进来;故:minSum[0]=arr[0]=3,minSumEnd[0]=i=0位置
在这里插入图片描述
总之呢,填写minSum与minSumEnd的两个条件,很简单!!!
来到i位置,先看看minSum[i+1]
(1)如果minSum[i+1] > 0,不好意思,后面是正的,扩进来就不是最小累加和了,所以不要i+1位置,
所以呢:minSum[i]=arr[i],minSumEnd[i]=i
(2)如果minSum[i+1] <= 0,美滋滋,后面是负的或0,能扩进来,要么我变小(遇负数),即使遇到0不变小,我也变长(i–j扩大了)
所以呢:minSum[i]=arr[i] + minSum[i+1],minSumEnd[i]=minSumEnd[i+1]
也就是扩后边一堆进来,而且结尾位置j秒杀拿到了,这样i–j就是最小累加和

是不是超级超级简单?

好,咱们手撕填写minSum[i],minSumEnd[i]的代码:

    //总之呢,填写minSum与minSumEnd的两个条件,很简单!!!
    public static int[][] writeMinSumAndEnd(int[] arr){
        //minSum是ans[0]行
        //minSum是sns[1]行
        //int[] minSum = ans[0];
        //int[] minSumEnd = ans[1];
        int N = arr.length;
        int[][] ans = new int[2][N];
        //最开始最后那个位置,填好的
        ans[0][N - 1] = arr[N - 1];
        ans[1][N - 1] = N - 1;

        //来到i位置,先看看minSum[i+1]
        for (int i = N - 2; i >= 0; i--) {
            //(1)如果minSum[i+1] **>** 0,不好意思,后面是正的,扩进来就不是最小累加和了,所以不要i+1位置,
            //所以呢:**minSum[i]=arr[i],minSumEnd[i]=i**
            if (ans[0][i + 1] > 0){
                ans[0][i] = arr[i];//minSum
                ans[1][i] = i;//minSumEnd
            }
            //(2)如果minSum[i+1] **<=** 0,美滋滋,后面是负的或0,能扩进来,要么我变小(遇负数),即使遇到0不变小,我也变长(i--j扩大了)
            //所以呢:**minSum[i]=arr[i] + minSum[i+1],minSumEnd[i]=minSumEnd[i+1]**
            //也就是扩后边一堆进来,而且结尾位置j秒杀拿到了,这样i--j就是最小累加和
            else {
                ans[0][i] = arr[i] + ans[0][i + 1];//minSum
                ans[1][i] = ans[1][i + 1];//minSumEnd
            }
        }

        return ans;
    }


arr的任意子数组累加和<=k的最大长度解题流程

有了这俩空间,咱们来换时间:

再捋一下啊:

minSum[i]是啥呢?把任意i位置开头的子数组,往后扩出来的最小累加和,放入minSum信息数组【i–j范围能扩得又长,累加和又小】
minSumEnd[i]是啥?还把这个最小累加和的尾部位置j记录在minSumEnd信息数组中

开篇咱们就说过,我们要把arr的i=0–N-1的每一个位置i,找到最小累加和,然后看它的长度,更新给max

来到位置i,咱们想知道i开头的子数组,累加和sum<=k,能有多长?更新给max的话,最后每个位置都有一个结果,整体max就出来了

来到i位置,咱们先把i–j秒杀扩进来,而不是每次都去遍历i–j一个一个求和哦!!!
sum=minSum[i],就相当于把i–j扩进来了
(1)若是发现sum<=k,说明,咱们还可以尝试往后扩x–y进来,因为后面更多的加入,也不一定sum就>k,如果x–y能进来,但是后面不能进来了,那y-i+1就是最长长度,将其更新给max
(2)若是发现sum=>k,说明,不能扩入x–y了,此时最长长度就是:j-i+1,将其更新给max
此时宣告i开头的子数组,最长就这么长了,没得扩了
(3)在(2)的基础上,扩不动了,那下一次尝试谁?i+1位置呗
此时我们不需要像暴力解一样,从i+1,i+2……一堆暴力累加,看看sum<=k否
而是只需要先将arr[i]从sum中剔除(先令sum=sum-arr[i]),就代表i+1开头的正式开始寻找了,
而且是直接从x位置开始扩,看看x能否被扩建最小累加和来i+1–j范围内全部舍弃,不要判断了
直接去看sum=sum+minSum[x]是否<=k?回到(1)

直接看下面例子来理解这个流程
在这里插入图片描述
sum=minSum[i]=minSum[5]=1,就相当于把i–j扩进来了
(1)发现sum=1<=k=2,说明,咱们还可以尝试往后扩x–y进来,因为后面更多的加入,也不一定sum就>k
故考虑让sum=sum+minSum[x=8]=1+4=5>k=2了,显然x–y没法扩进来,不就是下面条件(2)吗
(2)若是发现sum=>k,说明,不能扩入x–y了,此时最长长度就是:j-i+1=minSum[i]-i+1=7-5+1=3,将其更新给max=3
此时宣告i开头的子数组,最长就这么长了,没得扩了
(3)在(2)的基础上,扩不动了,那下一次尝试谁?i+1位置呗
我们要探索全局max,那下一次尝试i+1位置,看看它开头,能有更大的max出现吗?
只不过,此时我们不需要像暴力解一样,从i+1,i+2……一堆暴力累加,看看sum<=k否
而是只需要先将arr[i]从sum中剔除(先令sum=sum-arr[i]=1-3=-2),就代表i+1=6开头的正式开始寻找了,
而且是直接从x=8位置开始扩,i+1=6–j=8范围内全部舍弃,不要判断了
直接去看sum=sum+minSum[x]是否<=k?回到(1)
sum=sum+minSum[x]=-2+minSum[8]=-2+4=2>k,不好意思,没法扩,以6开头的最长长度,是x-6=8-6=2这不如之前那个3呢还……

为啥i=6开头时,6–7,7–7不去判断了?
因为当初i=5,5–7,3长度了,够长了,即使你现在剔除了arr[i],又找到了6–7,7–7,能满足sum<=k,那又如何? 你压根都没我长啊!
你达标又如何?根本没用,比我i=5开头那会的要短没意思啊!!!!……舍弃!舍弃!舍弃!
舍弃那些短的长度!
舍弃那些短的长度!
舍弃那些短的长度!
舍弃那些短的长度!

于是,这才让你直接从x=8开始扩的,如果x=8开始能扩进来,最次,最次也是8-6+1=3这么长,也不好比3更短的,所以我们舍弃6-j位置,直接扩x=8位置试试。

xiu……

怎么样?上面我们的思路明确了吧!
arr的i=0–N-1每个位置开头,收集一次长度,更新给max
i开头扩到j,不能扩下一个位置x了,那先把arr[i]剔除,然后让i+1开头尝试新的答案,但是i+1–j这些范围就舍弃掉!不要判断了!直接去看x能否扩进来,这样的话,新的长度,最次也是和之前一样长,或者更长

这就是本题的关键:舍弃短的范围长度,不要判断了,咱们节约大量时间,用额外空间换取时间加速!

我多次说了,但凡遇到舍弃思想的题目,都叫一个难!!!但是这种题,互联网大厂面试它还就真的爱考……
所以一定要搞透彻了!

手撕本题的代码:

    //arr的任意子数组累加和<=k的最大长度解题流程
    public static int mostFarLen(int[] arr, int k){
        if (arr == null || arr.length == 0) return 0;

        int N = arr.length;

        //填写minSum与minSumEnd的两个条件,很简单!!!
        int[][] min = writeMinSumAndEnd(arr);
        int[] minSum = min[0];
        int[] minSumEnd = min[1];

        int x = 0;//下一个x位置,最开始x=0位置,想把0扩进来,从左往右试
        int max = 0;
        int sum = 0;//先默认当前i开头的子数组累加和为0
        //来到i位置,咱们先把i--j秒杀扩进来,而**不是每次都去遍历i--j一个一个求和哦!!!**
        for (int i = 0; i < N; i++) {

            //使劲扩i--j,x--y,自开始x在i那
            while (x < N && sum + minSum[x] <= k){
                //(1)若是发现sum<=k,说明,咱们还可以尝试往后扩x--y进来,因为后面更多的加入,也不一定sum就>k,
                // 如果x--y能进来,但是后面不能进来了,那y-i+1就是最长长度,将其更新给max
                sum += minSum[x];sum+=minSum[i],就相当于把i--j搞进来
                x = minSumEnd[x] + 1;//本段的结尾是j,下一段的开头是j+1=x
            }
            //(2)若是发现sum=>k,说明,不能扩入x--y了,此时最长长度就是:j-i+1,将其更新给max
            //此时宣告i开头的子数组,最长就这么长了,没得扩了
            max = Math.max(max, x - i);//j+1=x,j-i+1=j+1-i=x-i
            //(3)在(2)的基础上,扩不动了,那下一次尝试谁?i+1位置呗
            //此时我们不需要像暴力解一样,从i+1,i+2……一堆暴力累加,看看sum<=k否
            //而是只需要**先将arr[i]从sum中剔除**(先令sum=sum-arr[i]),就代表i+1开头的正式开始寻找了,
            if (x > i) sum -= arr[i];//x保证要比i更大,这样才能舍弃i+1--j,去扩x
            else x = i + 1;//当x<=i了,当然需要x扩,探寻更长的长度,否则x永远到不了右边界
            //而且**是直接从x位置开始扩,看看x能否被扩建最小累加和来**,**i+1--j范围内全部舍弃,不要判断了**!
            //直接去看sum=sum+minSum[x]是否<=k?回到(1)
        }

        return max;
    }

    public static void test(){
        int[] arr = {-5,2,-3,2,3,6};
        System.out.println(maxLen(arr, 0));
        System.out.println(mostFarLen(arr, 0));
    }

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

看结果:

5
5

问题不大

本题是出了名的难,利用舍弃思想:提前准备额外信息数组,换取时间,达成优化和加速的目的!
从头到尾,咱们也讲了很了,为了不暴力从i–j去累加,提前准备最小累加和,放入minSum数组,和这个累加和minSum(i–j范围)的结尾位置j,放入minSumEnd数组
这样的话,来到i位置,使劲扩,扩到x位置扩不动了,x-i就是以i开头,最长的子数组,sum<=k达标。
现将arr[i]从sum剔除,意味着i+1位置开头的子数组有多长我们要去探索了,而且是直接从x位置开始扩,舍弃i+1–j这段更短的比较和判断 从而达到加速的目的

在代码中
x>i
因此每一个i,x都在使劲往后窜,不会回退,就让i循环走过o(n),整个结果答案就已经出来了,或者x先越界就出结果了
于是乎,时间复杂度o(n)


总结

提示:重要经验:

1)提前准备最小累加和,放入minSum数组,和这个累加和minSum(i–j范围)的结尾位置j,放入minSumEnd数组
2)来到i位置,使劲扩,扩到x位置扩不动了,x-i就是以i开头,最长的子数组,sum<=k达标。将arr[i]从sum剔除,意味着i+1位置开头的子数组有多长我们要去探索了,而且是直接从x位置开始扩,舍弃i+1–j这段更短的比较和判断, 从而达到加速的目的
3)笔试求AC,可以不考虑空间复杂度,但是面试既要考虑时间复杂度最优,也要考虑空间复杂度最优。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

冰露可乐

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

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

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

打赏作者

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

抵扣说明:

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

余额充值