2026-03-20:统计有序数组中可被 K 整除的子数组数量。用go语言,给定一个非降序排列的整数数组 nums,以及一个正整数 k。 定义:如果数组中一段连

0 阅读6分钟

2026-03-20:统计有序数组中可被 K 整除的子数组数量。用go语言,给定一个非降序排列的整数数组 nums,以及一个正整数 k。

定义:如果数组中一段连续、非空的子数组,它所有元素的和能被 k 整除,那么这个子数组就是良好子数组。

要求:统计数组中所有不同的良好子数组的个数(只要子数组的位置 / 序列不同,就算不同的子数组)。

1 <= nums.length <= 100000。

1 <= nums[i] <= 1000000000。

nums 为非降序排列。

1 <= k <= 1000000000。

输入: nums = [1,2,3], k = 3。

输出: 3。

解释:

良好子数组为 [1, 2]、[3] 和 [1, 2, 3]。例如,[1, 2, 3] 是良好的,因为其元素和为 1 + 2 + 3 = 6,且 6 % k = 6 % 3 = 0。

题目来自力扣3729。

代码执行过程详细分步描述(nums=[1,2,3],k=3)

首先明确核心知识点:

  1. 良好子数组:连续子数组和能被 k 整除;
  2. 前缀和sum[i] 表示数组前 i 个元素的累加和,子数组 [j+1, i] 的和 = sum[i] - sum[j]
  3. 整除判定(sum[i] - sum[j]) % k == 0 等价于 sum[i] % k == sum[j] % k
  4. 数组是非降序的,存在连续相同数字段,代码利用这个特性优化统计。

初始状态

  • 前缀和计数器 cnt{0:1}(初始化0的余数,对应前缀和为0的情况,是子数组判定的基础)
  • 当前前缀和 sum:0
  • 连续相同段起始下标 lastStart:0
  • 答案 ans:0
  • 数组元素:[1, 2, 3],k=3

第一步:遍历第1个元素(下标i=0,值x=1)

  1. 条件判断:i>0 不成立,不执行连续段统计逻辑;
  2. 更新前缀和:sum = 0 + 1 = 1
  3. 计算余数:1 % 3 = 1,查询计数器cnt中余数1的数量为0;
  4. 答案累加:ans = 0 + 0 = 0当前状态:sum=1,ans=0,lastStart=0,cnt={0:1}

第二步:遍历第2个元素(下标i=1,值x=2)

  1. 条件判断:i>0 成立,且x=2 != nums[0]=1,说明上一个连续段(仅元素1)结束
  2. 统计上一段:上一段长度=1-0=1,将对应前缀和余数加入计数器:
    • 上一段前缀和s=1,余数1%3=1,计数器cnt[1]变为1;
  3. 更新连续段起始:lastStart = 1
  4. 更新前缀和:sum = 1 + 2 = 3
  5. 计算余数:3 % 3 = 0,查询计数器cnt[0]=1
  6. 答案累加:ans = 0 + 1 = 1当前状态:sum=3,ans=1,lastStart=1,cnt={0:1, 1:1}

第三步:遍历第3个元素(下标i=2,值x=3)

  1. 条件判断:i>0 成立,且x=3 != nums[1]=2,说明上一个连续段(仅元素2)结束
  2. 统计上一段:上一段长度=2-1=1,将对应前缀和余数加入计数器:
    • 上一段前缀和s=3,余数3%3=0,计数器cnt[0]变为2;
  3. 更新连续段起始:lastStart = 2
  4. 更新前缀和:sum = 3 + 3 = 6
  5. 计算余数:6 % 3 = 0,查询计数器cnt[0]=2
  6. 答案累加:ans = 1 + 2 = 3当前状态:sum=6,ans=3,lastStart=2,cnt={0:2, 1:1}

遍历结束

最终答案为3,与题目输出一致。


核心逻辑总结

  1. 利用前缀和+余数相等的数学规律,快速判断子数组和能否被k整除;
  2. 利用数组非降序的特性,将连续相同的数字作为一个整体统计,避免重复计算;
  3. 遍历过程中,每遇到不同数字,就把上一段连续数字的前缀和余数存入计数器,再用当前前缀和余数匹配计数器,累加符合条件的子数组数量。

时间复杂度 & 额外空间复杂度

1. 时间复杂度:O(n)

  • 数组仅遍历一次,每个元素只会被加入计数器一次
  • 没有嵌套循环,总操作次数与数组长度n成正比,是线性时间复杂度。

2. 额外空间复杂度:O(min(n, k))

  • 核心占用空间是余数计数器map,存储的是前缀和的余数;
  • 余数的取值范围只有0 ~ k-1,最多存储min(n, k)个键值对;
  • 其他变量(sum、lastStart、ans)都是常数级空间;
  • 实际场景中k可能很大,空间复杂度等价于O(n)

总结

  1. 执行过程:遍历数组→分段统计连续相同元素→更新前缀和→匹配余数计数器→累加答案;
  2. 时间复杂度:O(n)(线性复杂度,适配10万长度的数组);
  3. 额外空间复杂度:O(min(n, k))(最优的空间优化方案)。

Go完整代码如下:

package main

import (
	"fmt"
)

func numGoodSubarrays(nums []int, k int) (ans int64) {
	cnt := map[int]int{0: 1} // 为什么加个 0?见 560 题
	sum := 0                 // 前缀和
	lastStart := 0           // 上一个连续相同段的起始下标
	for i, x := range nums {
		if i > 0 && x != nums[i-1] {
			// 上一个连续相同段结束,可以把上一段对应的前缀和添加到 cnt
			s := sum
			for range i - lastStart {
				cnt[s%k]++
				s -= nums[i-1]
			}
			lastStart = i
		}
		sum += x
		ans += int64(cnt[sum%k])
	}
	return
}

func main() {
	nums := []int{1, 2, 3}
	k := 3
	result := numGoodSubarrays(nums, k)
	fmt.Println(result)
}

在这里插入图片描述

Python完整代码如下:

# -*-coding:utf-8-*-

from typing import List

def num_good_subarrays(nums: List[int], k: int) -> int:
    """
    计算好子数组的个数
    好子数组定义:子数组的和能被 k 整除
    
    参数:
    nums: 整数数组
    k: 除数
    
    返回:
    好子数组的个数
    """
    cnt = {0: 1}  # 前缀和余数的计数器,初始化0出现1次
    prefix_sum = 0  # 当前前缀和
    last_start = 0  # 上一个连续相同段的起始下标
    ans = 0
    
    for i, x in enumerate(nums):
        # 如果当前元素与前一个元素不同,说明上一个连续段结束
        if i > 0 and x != nums[i-1]:
            # 处理上一个连续相同段
            s = prefix_sum
            # 将上一段中所有可能的前缀和余数添加到计数器
            for _ in range(i - last_start):
                cnt[s % k] = cnt.get(s % k, 0) + 1
                s -= nums[i-1]
            last_start = i
        
        prefix_sum += x
        # 当前前缀和余数在计数器中出现的次数就是好子数组的个数
        ans += cnt.get(prefix_sum % k, 0)
    
    return ans

def main():
    nums = [1, 2, 3]
    k = 3
    result = num_good_subarrays(nums, k)
    print(result)

if __name__ == "__main__":
    main()

在这里插入图片描述

C++完整代码如下:

#include <iostream>
#include <vector>
#include <unordered_map>
using namespace std;

long long numGoodSubarrays(vector<int>& nums, int k) {
    unordered_map<int, int> cnt;
    cnt[0] = 1; // 为什么加个 0?见 560 题
    int sum = 0; // 前缀和
    int lastStart = 0; // 上一个连续相同段的起始下标
    long long ans = 0;

    for (int i = 0; i < nums.size(); i++) {
        int x = nums[i];
        if (i > 0 && x != nums[i-1]) {
            // 上一个连续相同段结束,可以把上一段对应的前缀和添加到 cnt
            int s = sum;
            for (int j = 0; j < i - lastStart; j++) {
                cnt[s % k]++;
                s -= nums[i-1];
            }
            lastStart = i;
        }
        sum += x;
        ans += cnt[sum % k];
    }
    return ans;
}

int main() {
    vector<int> nums = {1, 2, 3};
    int k = 3;
    long long result = numGoodSubarrays(nums, k);
    cout << result << endl;
    return 0;
}

在这里插入图片描述