【纪中模拟2019.08.01】【JZOJ2644】数列
题意:
在一个长度为$N$的正整数序列$\{a_i\}$中,定义一个“合法”的子区间,其和能够被$K$整除。求原序列包括多少个“合法”的子区间?
共$T$组数据。
$1\le T\le 20,\quad 1\le N\le 5*10^4,\quad 1\le K \le 10^6,\quad 1\le a_i\le 10^9$
分析:
乍一看想不到$O(n\,logn)$的做法,先写一写$O(n)\sim O(n^2)$的暴力吧。
这里$O(n)$是预处理前缀和的时间,$O(n^2)$是枚举区间端点的时间。每次发现一个“合法”的子区间就$ans++$。
思考一下,我们这么做是基于这样一个式子:$$pre[r]-pre[l-1]=\sum\limits^{r}_{i=l}a_i\equiv 0\;(mod\,K)$$
那么将它改写一下,显然有$$pre[r]\equiv pre[l-1]\;(mod\,K)$$
垂死病中惊坐起,此题开桶我看行!前缀和模$K$同余的任意两个位置作为端点所组成的区间都是“合法”的,于是我们开桶对模$K$各个余数进行计数,运用组合数求答案。
时间复杂度O(K),空间复杂度O(N+K)。
实现(100分):
#include<iostream> #include<cstdio> #include<cstring> #include<cmath> #include<algorithm> #define IL inline using namespace std; const int N=5e4; const int K=1e6; typedef long long LL; int n,T; LL k,a[N+3],s[N+3],c[N+3],ans; int cnt[K+3]; int main(){ c[0]=c[1]=0; c[2]=1; for(int i=3;i<=N;i++) c[i]=c[i-1]+i-1; scanf("%d",&T); while(T--){ scanf("%lld%d",&k,&n); for(int i=1;i<=n;i++) scanf("%lld",&a[i]); memset(cnt,0,sizeof cnt); s[0]=0; cnt[0]=1; for(int i=1;i<=n;i++){ s[i]=(s[i-1]+a[i])%k; cnt[s[i]]++; } ans=0; for(int i=0;i<k;i++) ans+=c[cnt[i]]; printf("%lld\n",ans); } return 0; }
小结:
对于计数问题,我们通常可以把原理式进行变形,找到规律,就可以从暴力$ans++$进化了。