1248. 统计「优美子数组」

给你一个整数数组 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[ ]

下标:0123456
元素:2414368
类型:偶数偶数奇数偶数奇数偶数偶数

保存奇数下标的数组:odd[ ] = {2,4}

下标:0123456
元素:2414368
类型:偶数偶数奇数偶数奇数偶数偶数
odd[ ]24

现在通过 add[] 数组,我们可以把原数组分为三部分,如果 k=1 ,那么计算优美子数组的公式应该为

  • 左边: 2 号 下 标 的 左 右 : 3 × 2 2号下标的左右:3×2 23×2
  • 右边: 4 号 下 标 的 左 右 : 2 × 3 4号下标的左右:2×3 42×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个奇数的下标。左指针指向最左边的偶数下标,右指针指向做右边的偶数下标。

下标:0123456
元素:2414368
类型:偶数偶数奇数偶数奇数偶数偶数
指针/队列lfrontbackr

其中,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[i1] 递推而来,即:

pre [ i ] = pre [ i − 1 ] + ( nums [ i ] & 1 ) \textit{pre}[i]=\textit{pre}[i-1]+(\textit{nums}[i]\&1) pre[i]=pre[i1]+(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[j1]==k

简单移项可得符合条件的下标 j j j 需要满足

pre [ j − 1 ] = = pre [ i ] − k \textit{pre}[j-1] == \textit{pre}[i] - k pre[j1]==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 0ji 。同时,由于 pre [ i ] \textit{pre}[i] pre[i] 的计算只与前一项的答案有关,因此我们可以不用建立 pre \textit{pre} pre 数组,直接用 odd \textit{odd} odd 变量来记录 p r e [ i − 1 ] pre[i-1] pre[i1] 的答案即可。

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;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我叫RT

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值