k 倍区间(同余定理,组合数)

题目描述

给定一个长度为 N 的数列,1,2,⋯A1,A2,AN,如果其中一段连续的子序列 ,+1,⋯(≤)Ai,Ai+1,Aj(ij) 之和是 K 的倍数,我们就称这个区间 [,][i,j] 是 K 倍区间。

你能求出数列中总共有多少个 K 倍区间吗?

输入格式

第一行包含两个整数 N 和 K(1≤,≤105)(1N,K105)。

以下 N 行每行包含一个整数 Ai(1≤≤105)(1Ai105)。

输出格式

输出一个整数,代表 K 倍区间的数目。

输入输出样例

输入 #1
5 2
1  
2  
3  
4  
5  
输出 #1
6
本题的第一思路就前缀和,本以为时间限制是2s完全够我开n2,但还是tle了,想不到优化的方案
没想到使用数学知识来优化:
利用同余定理:当 a mod k=b mod k 时,abmodk=0。
若设前缀和数组为 s,则我们知道,si 就表示 0+1+2+...+a0+a1+a2+...+ai
那么我们用另一个数组存储每个前缀和的模数,当找到相同模数时,中间的那段即为 k 的倍数。
统计前缀和除以 k 后余数的数量,然后两两组合计算答案。
其实就是计算 ∑=0−1[]2i=0k1Ccnt[i]2​,利用组合数两两组合;
然后 00 的数量要特判一下。
比如计算区间 [1,5][1,5] 的和,应该是 sumd5sumd0,所以 sumd0 也要统计进去,而 sumd0=0
所以特别处理一下就可以了:
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
long long n,k,a[N],s[N],res,sum;
int main()
{
    cin>>n>>k;
    for(int i=1;i<=n;i++){
        cin>>a[i];
        sum=(sum+a[i])%k;
        s[sum]++;//利用桶来储存数量
    }
    s[0]++;//这里的0要特判一下,毕竟前面
    for(int i=0;i<k;i++)
        res+=s[i]*(s[i]-1)/2;//组合数两两组合公式
    cout<<res;
    return 0;
}

还有另外一种不适用组合数的方法:

每次取这个数组的前缀和与 k 取模,并加上这个前缀和出现的次数。如果不止一次出现,那么上一次出现的位置到这一次出现的位置之间的数的和就一定可以被 k 整除,即为 k 倍区间。

另外,如果这个前缀和已经可以直接被 k 整除的话,也算 k 倍区间。

输出最后的个数即可:

 

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
long long  a[N],res,s[N],n,k,ans[N];
int main()
{
    cin>>n>>k;
    for(int i=1;i<=n;i++){
        cin>>a[i];
        s[i]=(s[i-1]+a[i])%k;
        res+=ans[s[i]];//解释一下这里为什么要先加再++
        //因为同余定理是两个同余的数合并才是一个k倍区间
        //模拟一下,如果先++再加的话,此时余这个数的个数只有一个,不能算入k倍区间;
        ans[s[i]]++;
    }
    cout<<res+ans[0];
}
posted @ 2023-06-11 10:04  o-Sakurajimamai-o  阅读(61)  评论(0)    收藏  举报
-- --