930. 和相同的二元子数组
题目
给你一个二元数组 nums
,和一个整数 goal
,请你统计并返回有多少个和为 goal
的 非空 子数组。
子数组 是数组的一段连续部分。
示例 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]
示例 2:
输入:nums = [0,0,0,0,0], goal = 0
输出:15
提示:
1 <= nums.length <= 3 * 104
nums[i]
不是0
就是1
0 <= goal <= nums.length
思路
滑窗解法
将问题转换为 大于等于goal的数组个数 - 大于等于goal+1的数组个数 = 等于goal的数组个数
class Solution {
public int numSubarraysWithSum(int[] nums, int goal) {
return numSubarraysLargerSum(nums, goal) - numSubarraysLargerSum(nums, goal + 1);
}
private int numSubarraysLargerSum(int[] nums, int goal) {
int count = 0, left = 0, sum = 0;
for (int right = 0; right < nums.length; right++) {
sum += nums[right];
while (sum >= goal && left <= right) {
sum -= nums[left];
left++;
}
count += left;
}
return count;
}
}
前缀和解法
什么是前缀和?
以示例一为例说明,现在有一个前缀和数组presum[]
:
presum[1]
代表nums中前1位的和,也就是nums[0]
presum[2]
是前两位的和,presum[2] = nums[0] + nums[1] = presum[1] + nums[1]
不难看出有以下规律:presum[n] = presum[n-1] + nums[n-1]
通过前缀和获取某个区间的和
比如现在求[2,4]区间的和:
presum[3]=presum[2]+nums[2]
presum[4]=presum[3]+nums[3]
presum[5]=presum[4]+nums[4]=presum[3]+num[3]+num[4]=presum[2]+num[2]+num[3]+num[4]
可得num[2]+num[3]+num[4]=presum[5]-presum[2]
那如何利用前缀和解题?
我们假设有这样一个区间(i,j]
和刚好为goal。就有presum[j] - presum[i] = goal
,这样我们枚举j,记录和刚好为 goal 的 i 的出现次数,每次查询符合条件的 i 的数量即可。
class Solution {
public int numSubarraysWithSum(int[] nums, int goal) {
// 前缀和解法
// 先记录前缀和,如果想知道(left, right]区间的和,可以通过presum[right] - presum[left]来得到。
int[] presum = new int[nums.length + 1];
for (int i = 1; i < nums.length + 1; i++) {
presum[i] = presum[i - 1] + nums[i - 1];
}
HashMap<Integer, Integer> map = new HashMap<>();
map.put(0, 1);
int result = 0;
// presum[right] - presum[left] = goal,所以如果我们枚举右侧前缀和,只要减去goal,我们就可以知道需要值为多少的前缀和。
// 再从hash表中找到以这个值为和的前缀有多少个,就能知道在当前右指针情况下有几个数组和为goal了
for (int right = 0; right < nums.length; right++) {
int r = presum[right + 1], l = r - goal;
result += map.getOrDefault(l, 0);
map.put(r, map.getOrDefault(r, 0) + 1);
}
return result;
}
}