给你一个整数数组 nums 和一个整数 k。
如果某个 连续 子数组中恰好有 k 个奇数数字,我们就认为这个子数组是「优美子数组」。
请返回这个数组中「优美子数组」的数目。
示例 1:
输入:nums = [1,1,2,1,1], k = 3
输出:2
解释:包含 3 个奇数的子数组是 [1,1,2,1] 和 [1,2,1,1] 。
示例 2:
输入:nums = [2,4,6], k = 1
输出:0
解释:数列中不包含任何奇数,所以不存在优美子数组。
示例 3:
输入:nums = [2,2,2,1,2,2,1,2,2,2], k = 2
输出:16
提示:
1 <= nums.length <= 50000
1 <= nums[i] <= 10^5
1 <= k <= nums.length
来源:力扣(LeetCode) 1248. 统计「优美子数组」
链接:https://leetcode-cn.com/problems/count-number-of-nice-subarrays
分析:
- 把数组中满足要求的最小单元视为一个整体,如 [ 2, 1, 3, 6] , 如果 k 为 2 ,则数组中 {1,3} 可视为一个整体。那么可取的子数组为 [ 2, {1,3}, 6]、[ 2 , {1,3}]、[ {1,2} , 6] 三种。
- 发现规律,通过观察我们发现,在一个满足要求的最小单元左右有且只有偶数可选,并且这些偶数存在于一种选与不选之间的两种状态。
- 那么,对于 【2,4,{1,2,3},6】 而言,左边可选方案有3种,分别是 0(2,4都不选),1(选一个,2),2(选两个,2、4)。
- 右边可选方案有2种。0 (不选),1 (选6)
- 则,最终的结果一共有 3 × 2 = 6 种结果。
因此,我们根据左右可选偶数个数可以确定优美子数组的个数, 【左边偶数+1】× 【右边偶数+1】
法一:
通过一个额外的数组保存原数组中的奇数下标,其中每个下标中间的空缺下标一定是偶数的下标。
原数组:nums[ ]
下标: | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
元素: | 2 | 4 | 1 | 4 | 3 | 6 | 8 |
类型: | 偶数 | 偶数 | 奇数 | 偶数 | 奇数 | 偶数 | 偶数 |
保存奇数下标的数组:odd[ ] = {2,4}
下标: | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
元素: | 2 | 4 | 1 | 4 | 3 | 6 | 8 |
类型: | 偶数 | 偶数 | 奇数 | 偶数 | 奇数 | 偶数 | 偶数 |
odd[ ] | 2 | 4 |
现在通过 add[] 数组,我们可以把原数组分为三部分,如果 k=1 ,那么计算优美子数组的公式应该为
- 左边: 2 号 下 标 的 左 右 : 3 × 2 2号下标的左右:3×2 2号下标的左右:3×2
- 右边: 4 号 下 标 的 左 右 : 2 × 3 4号下标的左右:2×3 4号下标的左右:2×3
- 因此,最终有 3×2 + 2×3 = 12 个优美子数组。
如果后面还有满足要求的奇数列,我们可以通过循环按照相同的方式计算。
- 为了计算方便,我们在 add 数组的两边加上两个哨兵,{-1,n}。则add[ ] = {-1,2,4,6}。
C++实现:
int numberOfSubarrays(vector<int>& nums, int k) {
int n = (int)nums.size();
/*
* add保存奇数下标, add 两端设置哨兵 -1,n
*/
int odd[n + 2], ans = 0, cnt = 0;
for (int i = 0; i < n; ++i) {
if (nums[i] & 1) odd[++cnt] = i;
}
odd[0] = -1, odd[++cnt] = n; // 设置哨兵
for (int i = 1; i + k <= cnt; ++i) {
/* 左边 × 右边 */
ans += (odd[i] - odd[i - 1]) * (odd[i + k] - odd[i + k - 1]);
}
return ans;
}
法二:
法一借助数组实现,使用双指针的方法同样也可以实现,同时我们需要借助队列。
具体思路为:队头保存第一个奇数的下标,队尾保存第k个奇数的下标。左指针指向最左边的偶数下标,右指针指向做右边的偶数下标。
下标: | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
元素: | 2 | 4 | 1 | 4 | 3 | 6 | 8 |
类型: | 偶数 | 偶数 | 奇数 | 偶数 | 奇数 | 偶数 | 偶数 |
指针/队列 | l | front | back | r |
其中,l
表示左指针,front
表示队头。 front - l
就是左边之间偶数的个数,同样的,r - back
就是右边之间的偶数个数。
C++实现:
int numberOfSubarrays(vector<int>& nums, int k) {
queue<int> odd;
int l = -1, r = 0;
int ans = 0;
for( ; r < nums.size();; ++r)
{
if(nums[r] & 1) // 奇数
{
if(odd.size() == k) // 奇数个数 == k
{
ans += (odd.front() - l) * (r - odd.back());
l = odd.front();
odd.pop();
}
odd.push(r);
}
}
// 最后一个数是偶数,且满足要求
if(odd.size() == k) ans += (odd.front() - l) * (r - odd.back());
return ans;
}
法二:
前缀和 + 差分 。 这里引用自力扣官方题解。
考虑以 i
结尾的「优美子数组」个数,我们需要统计符合条件的下标 j
的个数,其中 0≤j≤i
且 [j..i]
这个子数组里的奇数个数恰好为 k 。如果枚举 [0..i]
里所有的下标来判断是否符合条件,那么复杂度将会达到 O(n^2)
,无法通过所有测试用例,因此我们需要优化枚举的时间复杂度。
我们定义 pre [ i ] \textit{pre}[i] pre[i]为 [ 0.. i ] [0..i] [0..i] 中奇数的个数,则 pre [ i ] \textit{pre}[i] pre[i] 可以由 pre [ i − 1 ] \textit{pre}[i-1] pre[i−1] 递推而来,即:
pre [ i ] = pre [ i − 1 ] + ( nums [ i ] & 1 ) \textit{pre}[i]=\textit{pre}[i-1]+(\textit{nums}[i]\&1) pre[i]=pre[i−1]+(nums[i]&1)
那么 [ j . . i ] [j..i] [j..i] 这个子数组里的奇数个数恰好为 k k k这个条件我们可以转化为
pre [ i ] − pre [ j − 1 ] = = k \textit{pre}[i]-\textit{pre}[j-1]==k pre[i]−pre[j−1]==k
简单移项可得符合条件的下标 j j j 需要满足
pre [ j − 1 ] = = pre [ i ] − k \textit{pre}[j-1] == \textit{pre}[i] - k pre[j−1]==pre[i]−k
所以我们考虑以 i i i 结尾的「优美子数组」个数时只要统计有多少个奇数个数为 pre [ i ] − k \textit{pre}[i]-k pre[i]−k 的 pre [ j ] \textit{pre}[j] pre[j] 即可。我们只要建立频次数组 cnt \textit{cnt} cnt 记录 pre [ i ] \textit{pre}[i] pre[i] 出现的次数,从左往右边更新 cnt \textit{cnt} cnt 边计算答案,那么以 i i i 结尾的答案 cnt [ pre [ i ] − k ] \textit{cnt}[\textit{pre}[i]-k] cnt[pre[i]−k] 即可 O ( 1 ) O(1) O(1) 得到。最后的答案即为所有下标结尾的「优美子数组」个数之和。
需要注意的是,从左往右边更新边计算的时候已经保证了 cnt [ pre [ i ] − k ] \textit{cnt}[\textit{pre}[i]-k] cnt[pre[i]−k] 里记录的 pre [ j ] \textit{pre}[j] pre[j] 的下标范围是 0 ≤ j ≤ i 0\leq j\leq i 0≤j≤i 。同时,由于 pre [ i ] \textit{pre}[i] pre[i] 的计算只与前一项的答案有关,因此我们可以不用建立 pre \textit{pre} pre 数组,直接用 odd \textit{odd} odd 变量来记录 p r e [ i − 1 ] pre[i-1] pre[i−1] 的答案即可。
int numberOfSubarrays(vector<int>& nums, int k) {
int n = nums.size();
vector<int> cnt(n + 1, 0); // 前缀表,保存当前元素之前奇数的个数
int odd = 0, ans = 0;
cnt[0] = 1;
for (int i = 0; i < n; ++i) {
odd += nums[i] & 1;
ans += odd >= k ? cnt[odd - k] : 0;
cnt[odd] += 1;
}
return ans;
}