题目描述

✅ 1248. 统计「优美子数组」

image-20260928230639911

image-20260928230639912

题意分析

统计恰好包含 k 个奇数的连续子数组数量。元素的具体数值不影响答案,只需把奇数看成 1、偶数看成 0,问题就变成统计区间和等于 k 的子数组。

解法:前缀奇数个数计数

核心思路

[!blue]

设 P[i] 为前 i 个元素中的奇数个数,P[0] = 0。区间 [l, r) 恰有 k 个奇数,等价于 P[r] - P[l] = k,也就是 P[l] = P[r] - k。

从左到右维护当前前缀奇数数 oddCount,用 prefixCount[c] 记录此前有多少个前缀包含 c 个奇数。处理完当前元素后,每个奇数数为 oddCount - k 的历史前缀都对应一个合法左端,因此它的频次就是当前右端新增的答案数。

不同前缀位置即使奇数数相同,也对应不同的子数组,所以要保存次数,不能只保存是否出现过或某一个下标。偶数虽然不增加 oddCount,仍会产生新的前缀位置,必须照常查询并登记。

初始 prefixCount[0] = 1 代表空前缀,用于统计从数组开头开始的区间。每轮先使用历史计数,再登记当前前缀;所有区间都按自己的右端点统计一次,不会重复。

解题步骤

  1. 前缀奇数数只会在 0 到 n 之间,创建长度为 n + 1 的频次数组,并登记一次空前缀。
  2. 遍历元素,遇到奇数才让 oddCount 加一。
  3. 若 oddCount >= k,把 prefixCount[oddCount - k] 加入答案;否则历史前缀不可能有负的奇数数,当前没有贡献。
  4. 不论当前元素是奇数还是偶数,都执行 prefixCount[oddCount]++,最后返回累计答案。

代码实现

class Solution {
    public int numberOfSubarrays(int[] nums, int k) {
        int[] prefixCount = new int[nums.length + 1];

        // 空前缀提供从数组下标零开始的子数组。
        prefixCount[0] = 1;

        int oddCount = 0;
        int ans = 0;

        for (int num : nums) {
            if (num % 2 == 1) {
                oddCount++;
            }

            // 历史前缀比当前少 k 个奇数,就能构成一个合法区间。
            if (oddCount >= k) {
                ans += prefixCount[oddCount - k];
            }

            // 偶数也产生新的前缀位置,同样需要登记。
            prefixCount[oddCount]++;
        }

        return ans;
    }
}
func numberOfSubarrays(nums []int, k int) int {
    prefixCount := make([]int, len(nums)+1)
    // 空前缀提供从数组下标零开始的子数组。
    prefixCount[0] = 1

    oddCount := 0
    ans := 0
    for _, num := range nums {
        if num%2 == 1 {
            oddCount++
        }

        // 历史前缀比当前少 k 个奇数,就能构成一个合法区间。
        if oddCount >= k {
            ans += prefixCount[oddCount-k]
        }
        // 偶数也产生新的前缀位置,同样需要登记。
        prefixCount[oddCount]++
    }

    return ans
}

复杂度分析

  • 时间复杂度:$O(n)$。
  • 空间复杂度:$O(n)$,前缀奇数数范围为零到 n。

关键点总结

[!green]

  • 频次代表不同前缀位置,不能只记录一个下标。
  • 每次右端都可能贡献答案,包括当前元素为偶数时。

易错点总结

[!yellow]

  • 缺少空前缀,会漏掉从数组开头起算的答案。
  • 只在遇奇数时登记前缀,会漏掉偶数形成的不同左端。
  • c 小于 k 时仍按 c−k 访问数组,会出现负下标。

相似题目

题目 难度 关联与区别
930. 和相同的二元子数组 中等 把奇数映射为1、偶数映射为0,恰有k个奇数就变成二元子数组和为k。
992. K 个不同整数的子数组 困难 同样可用atMost(k)-atMost(k-1)统计恰好条件,本题预算是奇数数量,原题是不同值种类数。
560. 和为 K 的子数组 中等 前缀和配合哈希表查找所需历史前缀;本题把奇数映射为 1 后统计精确数量,该题存前缀出现次数以统计精确和。
325. 和等于 k 的最长子数组长度 中等 前缀和配合哈希表查找所需历史前缀;本题把奇数映射为 1 后统计精确数量,该题存最早前缀下标以最大化长度。
437. 路径总和 III 中等 前缀和配合哈希表查找所需历史前缀;本题把奇数映射为 1 后统计精确数量,该题沿树路径维护前缀次数并回溯恢复。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2024/64589289
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!