LeetCode 3729. 统计有序数组中可被 K 整除的子数组数量

核心思路

本题的关键在于去重:两个子数组只要数值序列相同就视为同一个。由于数组有序,重复的子数组只能由连续相同的元素构成。

两步策略:

1. 统计全部(含重复):用前缀和 + 哈希表统计所有和能被 k 整除的子数组
2. 减去重复计数:对每段连续相同元素,减去重复统计的子数组数量

---

解法一:直接枚举(易理解)AC

```javascript
/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number}
 */
var numGoodSubarrays = function(nums, k) {
    // Step 1: 统计所有子数组(含重复)
    let ans = 0;
    let prefix = 0;
    const map = new Map();
    map.set(0, 1);
    
    for (let x of nums) {
        prefix = ((prefix + x) % k + k) % k;  // 处理负数
        const count = map.get(prefix) || 0;
        ans += count;
        map.set(prefix, count + 1);
    }
    
    // Step 2: 减去重复统计
    let i = 0;
    const n = nums.length;
    
    while (i < n) {
        let j = i + 1;
        while (j < n && nums[j] === nums[i]) j++;
        
        const m = j - i;      // 连续相同元素个数
        const val = nums[i];
        
        // 枚举所有可能的子数组长度
        for (let len = 1; len <= m; len++) {
            // 如果该长度组成的子数组和能被 k 整除
            if ((len * val) % k === 0) {
                // 该长度有 (m - len + 1) 个子数组,但在 step1 中统计了 m - len + 1 次
                // 只需要保留 1 个,所以要减去 m - len
                ans -= (m - len);
            }
        }
        
        i = j;
    }
    
    return ans;
};
```

---

解法二:数学优化(推荐)

利用 step = k / gcd(k, v) 跳跃枚举,避免遍历所有长度:

```javascript
/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number}
 */
var numGoodSubarrays = function(nums, k) {
    // 特判:k=1 时所有子数组都满足,不同子数组数量 = n
    if (k === 1) return nums.length;
    
    // Step 1: 统计所有子数组(含重复)
    let ans = 0;
    let prefix = 0;
    const map = new Map();
    map.set(0, 1);
    
    for (let x of nums) {
        prefix = ((prefix + x) % k + k) % k;
        const count = map.get(prefix) || 0;
        ans += count;
        map.set(prefix, count + 1);
    }
    
    // Step 2: 减去重复统计
    const n = nums.length;
    let i = 0;
    
    while (i < n) {
        let j = i + 1;
        while (j < n && nums[j] === nums[i]) j++;
        
        const m = j - i;      // 连续相同元素个数
        const val = nums[i];
        
        // 数学优化:只需要枚举能被 k 整除的长度
        // 步长 step = k / gcd(k, val)
        const step = k / gcd(k, Math.abs(val));
        
        // 从 step 开始,每次增加 step,直到 m
        for (let len = step; len <= m; len += step) {
            ans -= (m - len);
        }
        
        i = j;
    }
    
    return ans;
};

// 最大公约数(辅助函数)
function gcd(a, b) {
    a = Math.abs(a);
    b = Math.abs(b);
    while (b !== 0) {
        [a, b] = [b, a % b];
    }
    return a;
}
```

---

详细示例

```javascript
// 示例 1
console.log(numGoodSubarrays([4,5,0,-2,-3,1], 5));
// 输出:7
// 解释:所有子数组和为 5 的倍数,去重后有 7 个

// 示例 2
console.log(numGoodSubarrays([1,1,1,1], 2));
// 输出:4
// 解释:和为偶数的不同子数组:[1,1], [1,1,1,1], 长度为2的段有2个但相同只算1个

// 示例 3
console.log(numGoodSubarrays([0,0,0,0], 3));
// 输出:4
// 解释:[0], [0,0], [0,0,0], [0,0,0,0] 共4个不同子数组
```

---

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n + Σm) 最坏 O(n²) O(n)
解法二 O(n + Σ(m/step)) 最坏 O(n²) O(n)

---

关键细节

1. 前缀和取模:((prefix + x) % k + k) % k 确保余数非负
2. 去重逻辑:
   · 对于长度为 m 的连续相同段,长度为 len 的子数组有 (m - len + 1) 个
   · 步骤1统计了全部,我们只需要保留 1 个,所以减去 (m - len)
3. 数学优化:
   · len * val % k === 0 等价于 len 是 k / gcd(k, val) 的倍数
   · 只用枚举 len = step, 2*step, 3*step, ...
4. 边界情况:
   · val = 0 时,gcd(k, 0) = k,step = 1,所有长度都要去重
   · k = 1 时,直接返回 nums.length 即可

---

如果还想看其他语言的实现或有任何疑问,欢迎继续提问!

 

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐