一个二元数组 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