题目:
https://www.luogu.org/problemnew/show/3941
题意:给定一个n*m矩阵,求这个矩阵内有多少个子矩阵的和是k的倍数
n,m<=400,k<=10^6
题解
一开始的想法是枚举子矩阵,复杂度是n^4,能过60分
正解是脑洞??
考虑一行的情况,sum数组维护前缀和
对于区间[l,r] 区间和=sum[r]-sum[l-1]
(sum[r]-sum[l-1])%k=0;
sum[r]%k-sum[l-1]%k=0
sum[r]%k=sum[l-1]%k
那么枚举到r,1 ~ r-1有多少个sum对k取模的余数和sum[r]对k取模的余数相等,就有多少以r结尾的子区间满足题意
显然1 ~ r-1有多少个sum对k取模的余数和sum[r]对k取模的余数相等,也是可以用设置前缀和数组cnt维护的
这样我们就能O(n)的处理出一行的答案
枚举矩阵的上下边界,将多行压成一行,像上述处理出答案
复杂度n^3
细节:
要维护前缀和数组cnt,cnt的范围是0~k-1,在每次枚举上下边界时,cnt都要清空,而k的取值是很大的,直接枚举到k会tle
实际上,即使sum的模数都不同,cnt数组中也最多只有m个数字不为0
所以清空cnt的时候,枚举m,而不是枚举k
代码
#include<iostream>
#include<cstdio>
using namespace std;
const int N=400+50;
const int K=1000000+6;
long long sum[N][N],a[N][N],cnt[K],s[N];
long long n,m,k,ans;
int main(){
scanf("%lld%lld%lld",&n,&m,&k);
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
scanf("%lld",&a[i][j]);
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
sum[i][j]=sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1]+a[i][j];
sum[i][j]%=k;
// printf("%d %d = %d\n",i,j,sum[i][j]);
}
}
for(int i=0;i<=n;i++){
for(int j=i+1;j<=n;j++){
cnt[0]=1;
for(int x=1;x<=m;x++){
s[x]=sum[j][x]-sum[i][x];
while(s[x]<0) s[x]+=k;
s[x]%=k;
ans+=cnt[s[x]];
cnt[s[x]]++;
}
for(int x=1;x<=m;x++) cnt[s[x]]=0;//枚举到m
}
}
printf("%lld",ans);
return 0;
}