K倍区间
题目描述
核心思路
1.最简单的方法,采用暴力枚举,三层循环
for(int r = 1; r <= n; r++)
for(int l = 1; l <= n; l++)
{
int sum = 0;
for(int i = l; i <= r; i++)
{
·········计算区间和
}
判断是否可整除k
}
-
升级版1:优化最里面一层循环,将其优化,可以另外开辟一个数组sum,存前n项和,这样,里面可以变成sum[r]-sum[l-1]
for(int r = 1; r <= n; r++) for(int l = 1; l <= n; l++) { if((sum[r]-sum[l])%k==0) { } }
3.极度优化
由于是求某一段连续的子序列的总和,因此容易想到先用前缀和,来求出区间[1,n]中的前缀和。是K的倍数,说明%k为0。我们可以枚举右端点,从1枚举到N,当枚举到某个右端点R时,固定右端点,那么题目想问的就是区间[1,R]中有多少个L,使得能够满足 ( S [ R ] − S [ L − 1 ] ) % K = 0 (S[R]-S[L-1])\%K=0 (S[R]−S[L−1])%K=0,让区间都减去1,即区间为[0,R-1],那么问题就是:区间[0,R-1]中有多少个L,使得能够满足 ( S [ R ] − S [ L ] ) % K = 0 (S[R]-S[L])\%K=0 (S[R]−S[L])%K=0。由模运算的性质: ( a − b ) % k = ( a % k − b % k ) % k (a-b)\%k=(a\%k-b\%k)\%k (a−b)%k=(a%k−b%k)%k,因此有 ( S [ R ] % K − S [ L ] % K ) % K = 0 (S[R]\%K-S[L]\%K)\%K=0 (S[R]%K−S[L]%K)%K=0,即有 S [ R ] % K = S [ L ] % K S[R]\%K=S[L]\%K S[R]%K=S[L]%K。那么问题就转化为:有多少个 S [ L ] S[L] S[L],使得 S [ L ] % K S[L]\%K S[L]%K的余数与 S [ R ] % K S[R]\%K S[R]%K的余数相等。
我们开一个数组cnt,其中cnt[x]表示模K余数为x的数的个数。
问题:如何理解cnt[0]=1呢?
这里是用前缀和数组来算某个区间的和,前缀和数组s[0] = 0, s[i] = a[1] + a[2] + … + a[i],如果想算a[L] + a[L + 1] + … + a[R],它的值就等于s[R] - S[L - 1],所以当L = 1的时候,就会用到s[0]了,s[0]存的是模K余数为0的数的个数,由于s[0] = 0,所以最初等于0的有1个数,所以cnt[0] = 1。
问题:如何理解下面这段代码呢?
for (int i = 1; i <= n; i ++ )
{
res += cnt[s[i] % k];
cnt[s[i] % k] ++ ;
}
这里i是右端点,然后求有多少个左端点,使得区间内的数是k的倍数,1 ~ i的前缀和的余数是s[i] % k,所以满足要求的左端点的数量就是余数相同的前缀和数量,就是cnt[s[i] % k]。cnt[sum[r] % k] 表示余数是sum[r] % k 有多少个,亦即余数是sum[l] % k 的l有多少个(这里的r其实就是i)。
代码
#include<iostream>
using namespace std;
typedef long long LL;
const int N=100010;
int n,k;
LL s[N]; //前缀和
LL cnt[N]; //cnt[i]存储的是模k余数为i的数的个数
int main()
{
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++)
{
scanf("%lld",&s[i]);
s[i]+=s[i-1];
}
LL ans=0;
cnt[0]=1;//区间[0,0]中余数为0的数的个数有1个,就是0这个数字。
for(int i=1;i<=n;i++)//枚举右端点r
{
//就是说枚举到当前点i时,s[i]%k求出的是余数x,那么i之前可能已经出现过余数为x
//那么当枚举到当前右端点i时,cnt[x]就是区间[1,i-1]中出现过余数为x的数的个数
ans+=cnt[s[i]%k]; //拿出当前余数相同时,当前值之前余数的个数
//余数为x的数的个数在端点i出现了一次,则要加上
cnt[s[i]%k]++;//记录模k余s[i]%k的数的个数
}
printf("%lld\n",ans);
return 0;
}