给定一个长度为 N 的数列,A1,A2,…AN,如果其中一段连续的子序列 Ai,Ai+1,…Aj 之和是 K 的倍数,我们就称这个区间 [i,j] 是 K 倍区间。
你能求出数列中总共有多少个 K 倍区间吗?
输入格式
第一行包含两个整数 N 和 K。
以下 N 行每行包含一个整数 Ai。
输出格式
输出一个整数,代表 K 倍区间的数目。
数据范围
1≤N,K≤100000,
1≤Ai≤100000
输入样例:
5 2
1
2
3
4
5
输出样例:
6
思路分析
算法角度:通过区间和想到可以用前缀和.
如果我们想遍历所有的区间,可以用两个for循环:
for(int r=1;r<=n;r++)
for(int l=1;l<=r;l++)
ans=s[r]-s[l-1];
转化一下:
for(int r=1;r<=n;r++)
for(int l=0;l<r;l++)
ans=s[r]-s[l];
从这个双重for循环中我们能发现什么:当我们求得前缀和数组后(从0-n),在0-n中任取两个不同的数i,j(i<j),s[j]-s[i]即为一个区间和,当我们取完所有可能性的时候,我们就构成了所有区间的可能。
那么我们求完前缀和以后,区间和是k的倍数就可以转化为某两个前缀和之差为k的倍数,也就是说s[j]%k==s[i]%k,所以问题就迎刃而解:
1.统计前缀和数组中各个数之于k的余数
2.计算,相加
代码如下:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll maxn=1e5+10;
ll n,k;
ll a[maxn],s[maxn];
int main(){
scanf("%lld %lld",&n,&k);
for(ll i=1;i<=n;i++) scanf("%lld",&a[i]),s[i]=s[i-1]+a[i];
ll ans=0;
map<ll,ll> M;
for(ll i=1;i<=n;i++){
ll t=s[i]%k;
M[t]++;
}
ans+=M[0];
for(map<ll,ll>::iterator it=M.begin();it!=M.end();it++){
ll t=it->second;
ans+=(t-1)*t/2;
}
printf("%lld",ans);
return 0;
}
一般角度: 首先想到O(n^3)的算法:
for(int r=1;r<=n;r++)
for(int l=1;l<=r;l++){
int sum=0;
for(int k=l;k<=r;k++)
sum+=a[k];
if(sum%k==0) ans++;
}
利用前缀和优化:
for(int r=1;r<=n;r++)
for(int l=1;l<=r;l++){
int sum=s[r]-s[l-1];
if(sum%k==0) ans++;
}
考虑下边这个for循环,可以怎样转化?对,就是s[r]和s[l-1]对k取余的余数相同。所以内层for循环的作用就是判断i从0到r-1有多少个s[i]和s[r]的余数相同,因此我们可以用一个数组记录下来。
cnt[i]表示余数为i的数有多少个
int ans=0;
for(int r=1;r<=n;r++){
ans+=cnt[r%k];
cnt[r%k]++;
}
AC代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll maxn=1e5+10;
ll n,k;
ll a[maxn],s[maxn];
ll ans=0,cnt[maxn];
int main(){
scanf("%lld %lld",&n,&k);
for(ll i=1;i<=n;i++) scanf("%lld",&a[i]),s[i]=s[i-1]+a[i];
cnt[0]=1;
for(int r=1;r<=n;r++){
ans+=cnt[s[r]%k];
cnt[s[r]%k]++;
}
printf("%lld",ans);
return 0;
}
关键点
(a[l]+a[l+1]+…+a[r])%k==0 <=> ( s[r]-s[l-1])%k ==0 < = > s[r] %k ==s[l-1]%k