#include <iostream>
using namespace std;
int main()
{
// 请在此输入您的代码
int n,k;
cin>>n>>k;
int a[100005];
for(int i=0;i<n;i++){
cin>>a[i];
}
long long ans=0;
long long s = 0;
int cnt[100005]={0};
cnt[0]=1;
for(int i=0;i<n;i++){
s+=a[i];
int mod=s%k;
ans+=cnt[mod];
cnt[mod]++;
}
cout<<ans;
return 0;
}
cnt[100005]
是一个数组,用来记录前缀和模 k
的结果出现的次数。
为什么 cnt[0] = 1
?这是因为当 s % k == 0
时,表示从数组的开头开始,到当前位置 i
的子数组和已经是 k
的倍数。
-
s += a[i];
计算当前前缀和,s
是从数组的第一个元素到第i
元素的和。 -
mod = s % k;
计算当前前缀和模k
的值,即s
除以k
的余数。
如果某个前缀和的模 k
值已经出现过,那么说明从某个之前的点到当前位置的子数组和是 k
的倍数。
核心思路是利用前缀和的性质,通过模运算来高效地判断子数组是否为 k
的倍数。通过数组 cnt
来记录每个模 k
的前缀和出现的次数,每当相同的模值出现时,说明存在一个符合条件的子数
实例:
5 3
1
2
3
4
5
第一次循环 (i = 0
):
s += a[0] = 1
,当前前缀和为 1。mod = 1 % 3 = 1
,计算当前前缀和的模值。ans += cnt[mod] = ans + cnt[1] = 0 + 0 = 0
,由于cnt[1]
为 0,表示之前没有遇到过前缀和模值为 1 的情况。cnt[1]++
,更新cnt[1]
,现在cnt[1] = 1
。
第二次循环 (i = 1
):
s += a[1] = 1 + 2 = 3
,当前前缀和为 3。mod = 3 % 3 = 0
,计算当前前缀和的模值。ans += cnt[mod] = ans + cnt[0] = 0 + 1 = 1
,此时cnt[0] = 1
,说明之前有一个前缀和模值为 0 的子数组(即从数组开头到当前元素的子数组和已经是 3 的倍数),所以增加 1 个符合条件的子数组。cnt[0]++
,更新cnt[0]
,现在cnt[0] = 2
。
第三次循环 (i = 2
):
s += a[2] = 3 + 3 = 6
,当前前缀和为 6。mod = 6 % 3 = 0
,计算当前前缀和的模值。ans += cnt[mod] = ans + cnt[0] = 1 + 2 = 3
,此时cnt[0] = 2
,表示之前有两个前缀和模值为 0 的子数组,因此可以增加 2 个符合条件的子数组。cnt[0]++
,更新cnt[0]
,现在cnt[0] = 3
。
第四次循环 (i = 3
):
s += a[3] = 6 + 4 = 10
,当前前缀和为 10。mod = 10 % 3 = 1
,计算当前前缀和的模值。ans += cnt[mod] = ans + cnt[1] = 3 + 1 = 4
,此时cnt[1] = 1
,表示之前有一个前缀和模值为 1 的子数组,因此可以增加 1 个符合条件的子数组。cnt[1]++
,更新cnt[1]
,现在cnt[1] = 2
。
第五次循环 (i = 4
):
s += a[4] = 10 + 5 = 15
,当前前缀和为 15。mod = 15 % 3 = 0
,计算当前前缀和的模值。ans += cnt[mod] = ans + cnt[0] = 4 + 3 = 7
,此时cnt[0] = 3
,表示之前有三个前缀和模值为 0 的子数组,因此可以增加 3 个符合条件的子数组。cnt[0]++
,更新cnt[0]
,现在cnt[0] = 4
。
最终结果:
- 输出结果
ans = 7
,即符合条件的子数组总共有 7 个。