有元素和限制的最长子数组问题求解

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


问题

给定一个无序数组arr,其中元素可正、可负、可0。给定一个整数k,求arr所有的子数组中累加和小于或等于k的最长子数组长度

例如:arr = [3, -2, -4, 0, 6], k = -2. 相加和小于等于-2的最长子数组为{3, -2, -4, 0},所以结果返回4


输入描述:

第一行两个整数N, k。N表示数组长度,k的定义已在题目描述中给出
第二行N个整数表示数组内的数

输出描述:

输出一个整数表示答案

备注

1<=N<=10^{5}

-10^{9}<=K<=10^{9}

-100<=arri<=100

一、问题分析

        首先最容易想到的就是暴力,遍历每一个arr[i]求其往后累加满足条件(和小于等于K)的长度,再从每一个arr[i]对应的长度中求最大的,这种必定超时。所以,我们要充分利用每一步能够去记录的值,通过每一步多做一点事情来优化循环的总次数。

二、二分查找+前缀和O(nlgn)

用前缀和+二分查找很好理解,寻找存在满足条件的最长子数组核心是:找到>= sum − k 最早出现的位置。题目转换为:在长度为 n 的数组中,快速找到最早的 >= sum − k 的位置,

其思路是:

1、用sum{i}表示遍历到当前位置的前缀和

2、建立升序数组(目的是为了用二分查找)help[i]升序数组表示从头开始到当前位置i最大累加和

3、如果当前位置累加和为sum{i} 而如果 i 前面某个位置 j 满足help[j]>=sum{i}-k的话,那么从位置j+1到 i 这个区间的和一定小于等于K ,此时记录这个长度 len = i - j ,也就是遍历到当前位置前面满足条件的长度,最后在求所有满足条件中最长的那个一个就是答案

4、为什么要求最大累加和数组help[i] 而不是普通累加和 ?再次提醒因为要用二分查找者也是建立升序数组 help 的目的所在 ,另外注意是在找某个位置 j (满足 help[j]>=sum{i}-k) 肯定是找第一次满足条件的位置。如果你到这还没有自动理解为什么要找第一次满足条件的位置,那么请重新仔细阅读此方法的原理 。因为数组 help 里面记录从头到当前位置 最大累加和    help有重复元素 ,假设 help 有序列{.........a,a,a,a,a........}在每一组重复元素中最前面(也就是第一次出现的位置)才是严格从开始位置到这个位置累加和为 a 的位置

5、核心找出第一次满足条件的位置 (help[j]>=sum{i}-k) 用二分查找

代码如下:

int find_lower_bound(int a[],int end,int val){
    int l=0,right=end,mid;
    while(l<right){
        mid=(l+right)>>1;
        if(a[mid]>=val){
            right=mid;
        }else{
            l=mid+1;
        }
    }
    return l;
} 

void fun1(int n,int k){
    int sum = 0;
    int val, ret = 0;
    for ( int i = 1; i <= n; ++i ) {
        scanf("%d", &val);
        sum += val;
        int p = find_lower_bound(help, i, sum - k );//使用二分查找找到第一次大于等于sum-k的位置 
        ret = max( ret, i - p );                             
        help[i] = max( sum, help[i - 1] );   //因为要使用二分查找 所以建立升序数组help  
    }                                        // help[i]表示以i结尾前面累加最大和  
    printf("%d\n", ret);
}

三、双指针错误解法(漏洞解法)

在网上发现有作者说:计算以当前元素作为结尾最长的连续子数组长度,但是使用另外一种不消耗内存的方法,即双指针法。简单地说就是固定左指针,动右指针,当两指针之间的元素和大于了给定数,则移动右指针,直到两指针内部元素之和小于或等于了给定数为止


void fun2(int n,int s){	
        int num = 0;
        int sum = a[1];
        int i = 1;
        int j = 2;
        for(int _i=1;_i<=n;_i++)
        {
                //找到第一个小于S的下标 计为i     		
      		if(a[_i]<=s)        //找到第一个小于S的数记为sum 长度num=1 
      		{
      			i = _i;
      			j = _i+1;
      			sum = a[_i];
      			num = 1;    
      			break;
      		}
      		
      	}
        while (j <= n) {
            if (sum + a[j] <= s) {
                sum += a[j++];
                num = max(num, j - i);
            } else {
                sum += a[j++];
                while (i <= j && sum > s) {
                    sum -= a[i];
                    i++;
                }
            }
        }
	printf("%d", num); 
 
}

这种解法开头先初始化第一个位置为小于K的位置 ,这个没有错 但错误就出现在while里面

while (j <= n) {
            if (sum + a[j] <= s) {
                sum += a[j++];
                num = max(num, j - i);
            } else {
                sum += a[j++];
                while (i <= j && sum > s) {
                    sum -= a[i];
                    i++;
                }
            }
        }

当满足条件 右指针向后移动 但每次移动一个单位导致一旦出现加上sum+a[j] >s 而 加上 a[j] 后面一个元素满足条件sum+a[j+1] <=s 时 ,这时候忽略了sum+a[j+k] <=s 而移动左指针 ,也就是说一旦碰到加上这个元素就不满足条件sum+a[j]<=s 就移动了左指针  

 正确应该输出 6(第二行是正解) 而错误解法输出4(第三行) 原因就是 {0+2+5-3+4}=8>给定的 7  而移动了左指针 实际上

{0+2+5-3+4-2}=6<7 满足条件应该此时输出的长度为 6 

四 、史诗级O(n)滑动窗口思想

先附上图片


 光看图片好像效果像暴力遍历一样 其实不是  

1、我们建立数组 h[i] 表示从 i 位置往后遍历最小和为 h[i]

2、 ends[i] 表示从i 位置往后加到最小和的结束位置 ends[i] 

3、从前往后遍历 第一次从头开始借助h[i] 和 ends[i] 来找到最远位置 长度len=ends[i]+1-i  第一次结束位置即不满足条件的位置 此时可以像解法二那样移动左指针 与解法二的区别就是我们的右指针不是一个一个地移动 而是借助提前算好的最小累加和 h[i] 来计算满足条件的结束位置   

4、第一次找到了最远满足条件(累加和小于等于K)的位置 这时候 向右滑动窗口  右边继续向右延申 而左边弹出元素 直到右边到达边界为止

注意特殊情况

1、end如果没有移动 也就意味着此时sum加上当前位置往后最小累加和没有满足条件,

 sum就没有加上这个元素 因此也不需要弹出左边的 因为左边的压根没加进sum 

2、如果进了while end就从end加一开始 如果开始就没有延申出去 end就从i+1开始

代码:


void fun2(int n,int k){ //滑动窗口思想  
	  // special case
    if (n < 1)
        return ;
    int arr[n];
    for (int i=0; i<n; i++){
        cin >> arr[i];
    }
    
    int h[n], ends[n];
    h[n-1] = arr[n-1];
    ends[n-1] = n-1;
    
    // 找到从i出发的最长的最小值子队列
    for(int i=n-2; i>-1; i--){
        if(h[i+1]<=0){
            h[i] = arr[i] + h[i+1];
            ends[i] = ends[i+1];
        }
        else{
            h[i] = arr[i];
            ends[i] = i;
        }
    }
    
    // 从前往后,依次寻找和比较从位置i出发,满足要求小于等于k的子数组并比较长度
    int len=0, end=0, sum=0;
    for(int i=0; i<n; i++){
        while(end<n && (sum+h[end]<=k)){
            sum += h[end];
            end = ends[end]+1;
        }
        len = max(len, end-i);         // 注意上面的循环退出条件,这里end实际上已经在符合条件的区间外了,所以无需再+1
        sum -= end > i ? arr[i]: 0;    // 同样注意上面的循环条件,如果end压根没动,sum值则未更新,不需要进行再次清零操作
        end = max(i+1, end);           // 同理,此时end恰好在边界外,无需+1
    }
    cout << len;
}

总结

多想 多思考 多琢磨 多抽象成过程 

链接:未排序数组中累加和小于或等于给定值的最长子数组长度__牛客网
来源:牛客网

                                                                                                                                                       

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值