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)
首先明确核心知识点:
- 良好子数组:连续子数组和能被 k 整除;
- 前缀和:
sum[i]表示数组前 i 个元素的累加和,子数组[j+1, i]的和 =sum[i] - sum[j]; - 整除判定:
(sum[i] - sum[j]) % k == 0等价于sum[i] % k == sum[j] % k; - 数组是非降序的,存在连续相同数字段,代码利用这个特性优化统计。
初始状态
- 前缀和计数器
cnt:{0:1}(初始化0的余数,对应前缀和为0的情况,是子数组判定的基础) - 当前前缀和
sum:0 - 连续相同段起始下标
lastStart:0 - 答案
ans:0 - 数组元素:[1, 2, 3],k=3
第一步:遍历第1个元素(下标i=0,值x=1)
- 条件判断:
i>0不成立,不执行连续段统计逻辑; - 更新前缀和:
sum = 0 + 1 = 1; - 计算余数:
1 % 3 = 1,查询计数器cnt中余数1的数量为0; - 答案累加:
ans = 0 + 0 = 0; 当前状态:sum=1,ans=0,lastStart=0,cnt={0:1}
第二步:遍历第2个元素(下标i=1,值x=2)
- 条件判断:
i>0成立,且x=2 != nums[0]=1,说明上一个连续段(仅元素1)结束; - 统计上一段:上一段长度=1-0=1,将对应前缀和余数加入计数器:
- 上一段前缀和
s=1,余数1%3=1,计数器cnt[1]变为1;
- 上一段前缀和
- 更新连续段起始:
lastStart = 1; - 更新前缀和:
sum = 1 + 2 = 3; - 计算余数:
3 % 3 = 0,查询计数器cnt[0]=1; - 答案累加:
ans = 0 + 1 = 1; 当前状态:sum=3,ans=1,lastStart=1,cnt={0:1, 1:1}
第三步:遍历第3个元素(下标i=2,值x=3)
- 条件判断:
i>0成立,且x=3 != nums[1]=2,说明上一个连续段(仅元素2)结束; - 统计上一段:上一段长度=2-1=1,将对应前缀和余数加入计数器:
- 上一段前缀和
s=3,余数3%3=0,计数器cnt[0]变为2;
- 上一段前缀和
- 更新连续段起始:
lastStart = 2; - 更新前缀和:
sum = 3 + 3 = 6; - 计算余数:
6 % 3 = 0,查询计数器cnt[0]=2; - 答案累加:
ans = 1 + 2 = 3; 当前状态:sum=6,ans=3,lastStart=2,cnt={0:2, 1:1}
遍历结束
最终答案为3,与题目输出一致。
核心逻辑总结
- 利用前缀和+余数相等的数学规律,快速判断子数组和能否被k整除;
- 利用数组非降序的特性,将连续相同的数字作为一个整体统计,避免重复计算;
- 遍历过程中,每遇到不同数字,就把上一段连续数字的前缀和余数存入计数器,再用当前前缀和余数匹配计数器,累加符合条件的子数组数量。
时间复杂度 & 额外空间复杂度
1. 时间复杂度:O(n)
- 数组仅遍历一次,每个元素只会被加入计数器一次;
- 没有嵌套循环,总操作次数与数组长度n成正比,是线性时间复杂度。
2. 额外空间复杂度:O(min(n, k))
- 核心占用空间是余数计数器map,存储的是前缀和的余数;
- 余数的取值范围只有
0 ~ k-1,最多存储min(n, k)个键值对; - 其他变量(sum、lastStart、ans)都是常数级空间;
- 实际场景中k可能很大,空间复杂度等价于O(n)。
总结
- 执行过程:遍历数组→分段统计连续相同元素→更新前缀和→匹配余数计数器→累加答案;
- 时间复杂度:O(n)(线性复杂度,适配10万长度的数组);
- 额外空间复杂度: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;
}