计算和相同的子数组(详解)——力扣算法题,一步步分析与优化 day 1

一个二元数组 nums ,和一个整数 goal ,统计并返回有多少个和为 goal 的 非空 子数组。子数组 是数组的一段连续部分。nums[i] 不是 0 就是 1

要求输入:nums = [1,0,1,0,1], goal = 2

则输出 :4,(表示有这4个子数组[1,0,1]、[1,0,1,0]、[0,1,0,1]、[1,0,1])

--------------------------------------------for循环暴力统计---------------------------------------------------------------

分析:看到统计个数的,

思路一:暴力循环,两个指针从左往右遍历,每次循环当求和相等count++,大于求和就跳出循环

public static int numSubarraysWithSum(int[] nums, int goal) {
        int count = 0;
        for(int i = 0; i<=nums.length-1; i++){
            int sum = 0;
            for(int j = i ; j <= nums.length-1; j++){
                sum += nums[j];
                if(sum==goal){
                    count++;
                }else if(sum>goal){
                    break;
                }
            }
        }
        return count;
    }

执行用时1828ms,内存消耗41.6MB

时间复杂度O(n^2),空间复杂度O(1),显然这是不行的,因为在for循环的时候,反复计算了右边数字的累加,优化的方法有动态规划的思路把每次计算累加的存储起来,只需要一次循环就能返回结果。

那么如何存每次累加的结果?

---------------------------------------------------动态规划-------------------------------------------------------------

首先定义两把尺子,一把向右不停的移动,另一把向右累加求和,大于目标的时候,往左缩短,当等于目标的时候就停止,记录这把尺子的左边下标。

public static int numSubarraysWithSum(int[] nums, int goal) {
        int n = nums.length;
        int ans = 0;
        for (int r = 0, l = 0, s = 0; r < n; r++) {
            s += nums[r];
            while (l <= r && s >= goal) {//大于目标往开始往左缩进
                if(s == goal){
                    ans++;
//                    int len = r-l+1;
//                    System.out.println("下标的位置= "+l+" 尺子的长度 = "+len);
                }
                s -= nums[l++];//尺子往左缩进
            }

        }
        return ans;
    }

这种思路显然是可行的,可以解决不为零的数组情况

但是本题的条件:数组由0和1组成,由于0的存在,意味着等于goal值的情况有多种,左边的下标可能还会向左缩短,解决的思路是:再多加一把尺子,向左平滑

我们按照前面的思路,把尺子L分成L1、L2,现在就有三把尺子

初始指针为R,它每次向右平移,移动的长度就是L1、L2尺子能活动的范围。

L1存累加等于目标的值,一旦大于就会往左缩进

L2存累加小于目标的值,一旦大于等于就会往左缩,(L2的作用就是寻找等于目标时候,再往左缩进多少个单位刚好小于目标)
因此:L1尺子的长度 - L2尺子的长度就是目标的个数,

数学公式就是:L2-L1 = 目标个数

public int numSubarraysWithSum(int[] nums, int goal) {
        int n = nums.length;
        int ans = 0;
        for (int r = 0, l1 = 0, l2 = 0, s1 = 0, s2 = 0; r < n; r++) {
            s1 += nums[r];
            s2 += nums[r];
            while (l1 <= r && s1 > goal) s1 -= nums[l1++]; //大于目标左缩进
            while (l2 <= r && s2 >= goal) s2 -= nums[l2++];//大于等于目标左缩进
            ans += l2 - l1;
        }
        return ans;
    }

执行用时间2ms,内存消耗 41.4MB,那么有没有进一步优化的可能呢,再进一步优化就只能从数学推导开始。

-------------------------------------------数学方法之排列组合----------------------------------------------------------

由于数组不是0就是1,统计1的个数,用排列组合的方法解决

分类讨论:假设数组长度为n

先讨论极端情况:

情况1、当数组全为0,goal = 0时,组合的方法为n+(n-1)+......+1 ,一共有n*(n+1)/2,

情况2、goal=0的时候,数组不全为0,如:00010001000的时候,在每个连续的零之间,同样用1的方法,分段求出每段的排列,最后累加

情况3、goal>0 时候,数组全为1的时候,一共有n-goal+1种方法

情况4、goal>0的时候,数组不全为0的时候,记录1的下标,当有goal个1的时候,在(第一个1前面0的个数+1)*(最后一个1后面0的个数 +1),就是要计算的个数

于是,可以先建一个存1下标的数组,为了方便计算,我们可以在存下标为1的数组前面,开头存一个-1,最后存一个n

例如数组为[0,1,0,0,1,0,0] 存1的下标为[-1,1,4,7] ,但是存下标太麻烦了,我们只需要知道每个1前面0的个数,相减可以变成进一步将变成[2,3,3],这样就记录了每个1前面0的个数+1,当goal= 0的时候,累加每个分段的,用情况2求得,goal大于0,用情况4求

class Solution {
    public int numSubarraysWithSum(int[] nums, int goal) {
        int n = nums.length,size=0,last = -1;
        for (int i = 0; i < n; i++) {
            if(nums[i]==1){
                nums[size++]=i-last; //在第-1位置填充1
                last=i;
            }
        }
        if(size==n){ //当数组全为1的时候
            return (goal==0)?0:(n-goal+1); //目标值为0返回0  ,不为0返回n-goal+1
        }
        nums[size++]=n-last; //在第n个位置填充了1
        int ans=0;
        if(goal==0){ //当目标为0
            for (int i = 0; i < size; i++) {
                if(nums[i]>0){ //数组存的是每个1前面有多少个0
                    ans += (nums[i]-1)*nums[i]/2; //情况2
                }
            }
        }else{
            for (int i = goal; i < size; i++) { //直接从第goal个1开始计数
                ans += nums[i-goal]*nums[i];  //情况4
            }
        }
        return ans;
    }
}

执行用时间1ms,内存消耗 41.4MB

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值