bzoj4815[CQOI2017]小Q的格子
小Q是个程序员.
作为一个年轻的程序员,小Q总是被老C欺负,老C经常把一些麻烦的任务交给小Q来处理.
每当小Q不知道如何解决时,就只好向你求助.
为了完成任务,小Q需要列一个表格,表格有无穷多行,无穷多列,行和列都从1开始标号.
为了完成任务,表格里面每个格子都填了一个整数.
为了方便描述,小Q把第a行第b列的整数记为f(a,b).
为了完成任务,这个表格要满足一些条件:
(1)对任意的正整数a,b,都要满足f(a,b)=f(b,a)
(2)对任意的正整数a,b,都要满足b*f(a,a+b)=(a+b)*f(a,b)
为了完成任务,一开始表格里面的数很有规律,第a行第b列的数恰好等于a*b,显然一开始
是满足上述两个条件的.
为了完成任务,小Q需要不断的修改表格里面的数,每当修改了一个格子的数之后,为了让
表格继续满足上述两个条件,小Q还需要把这次修改能够波及到的全部格子里都改为恰当
的数.
由于某种神奇的力量驱使,已经确保了每一轮修改之后所有格子里的数仍然都是整数.
为了完成任务,小Q还需要随时获取前k行前k列这个有限区域内所有数的和是多少,答案可
能比较大,只需要算出答案mod1,000,000,007之后的结果。
数据范围:100%的数据1<=m<=10^4,1<=a,b,k<=n<=4*10^6,0<=x<10^18
分析
一句话题解:这道题是数论题
n<=4*106,无法直接存储n2大小的矩阵.因此我们猜测这道题有一些优雅的性质使得我们可以较为简单地求和.
给出的两个条件相当简单,简单到让人乍一看无处下手.要做出这道题,就要先分析这两个条件
首先我们需要仔细考虑"这次修改能够涉及到的全部格子"是什么意思.
也就是说,到底符合什么性质的格子之间会有影响.
"两个格子互相影响"的含义是修改一个格子会导致另一个格子被修改.
用<=>这个符号表示"互相影响"
根据条件(1),(a,b)<=>(b,a)
根据条件(2),(a,b)<=>(a,a+b)
结合(1)(2), (a,b)<=>(a+b,a)
令c=a+b,那么(a,b)<=>(a,a+b)可以写作(a,c-a)<=>(a,c)
也就是说,(a,b)<=>(a,a-b),此处不妨令a>b
对两个条件做了这么一些分析后,我们发现,把横坐标加上/减去纵坐标,或者把纵坐标加上/减去横坐标,
得到的格子和原先的格子互相影响.
如果熟练掌握了辗转相减法,我们就知道两个简单的性质:gcd(x,y)=gcd(x,x+y)=gcd(x,x-y)
也就是说,对于两个格子A(xa,ya),B(xb,yb),如果A<=>B,那么gcd(xa,ya)=gcd(xb,yb)
换一种说法,两个格子相互影响,当且仅当第一个格子横纵坐标的gcd=第二个格子横纵坐标的gcd.
也可以说,横纵坐标的gcd=1的所有格子之间互相影响,gcd=2的所有格子互相影响,gcd=3的格子...
我们还没有分析透这两个条件,因为我们没有利用第二个条件中的定量关系.
将条件(2)移项,得到f(a,a+b)/(a+b)=f(a,b)/b
也就是说,横坐标相同的格子,如果之间有影响,数值和纵坐标成正比.
同理,纵坐标相同的格子,如果之间有影响,数值和横坐标成正比.
横纵坐标都不相同的格子怎么办?注意如果这两个格子相互影响,我们总可以通过多次在同一行内或同一列内的变换从一个格子"走到"另一个格子,每一步都满足数值与横/纵坐标成正比,那么这两个格子的数值与它们横纵坐标的乘积成正比.也就是说:如果gcd(a,b)=gcd(x,y),那么f(a,b)/(a*b)=f(x,y)/(x*y)
再仔细考虑.在相互影响的一些格子里,必然存在一个数值最小的.如果这些格子横纵坐标的gcd均为g,那么f(g,g)是这些格子中数值最小的一个,且其他格子的数值都可以表示为f(g,g)的倍数.
这就启发了我们的解题思路:所有格子都可以表示为某一个f(g,g)的倍数,那么对于查询操作我们枚举g,求出每一个f(g,g)对答案的贡献.
(也就是说,对于所有横纵坐标的gcd=g的格子(x,y),我们把(x/g)*(y/g)
求和,乘上f(g,g)就得到了f(g,g)对答案的贡献)
于是我们存储所有的f(g,g),这只需要O(n)的空间,解决了n^2大小的矩阵无法直接存储的问题.
对于修改操作,我们容易找到修改的格子(x,y)所影响的那个f(g,g),把f(x,y)的新数值除以对应的系数(x/g)*(y/g)
,就得到了f(g,g)的新数值.
接下来我们需要解决的问题是:快速求解每个f(g,g)对答案的贡献.
注意n,m的乘积可以达到4*10^10
,如果对于每个询问枚举g,即使单个的g可以在O(1)内求解,时间上也无法承受.因此到现在我们可以认定这道题是一道数论题
要统计的式子是:
int ans=0;
for(g=1;g<=k;++g)
for(i=1;i<=k;++i)
for(j=1;j<=k;++j)
if(gcd(i,j)==g)ans+=f(g,g)*(i/g)*(j/g);
return ans;
//pseudocode,don't care about module or overflow
震惊!辣鸡博主连求和符号都不会打还来写博客骗访问量!
根据gcd计数的套路,因为是1<=i<=k,1<=j<=k,两维的大小均为k,我们考虑利用欧拉函数进行计数.但这个题里欧拉函数出现的形式并不显然.
不妨先考虑问题的简化版本:
int ans=0;
for(g=1;g<=k;++g)
for(i=1;i<=k;++i)
for(j=1;j<=k;++j)
if(gcd(i,j)==g)ans+=f(g,g);
return ans;
//pseudocode,don't care about module or overflow
这个形式里,f(g,g)在答案中贡献了多少个f(g,g)是一目了然的:就是1<=i<=k,1<=j<=k且gcd(i,j)=g的(i,j)对数
.莫比乌斯反演可以推导出来,但本题用欧拉函数更为简便.
i和j必然都是g的倍数.那么从1到k,g的倍数有[k/g]个,因此(i,j)可以表示为(p*g,q*g),1<=p,q<=[k/g]
.如果满足条件,需要gcd(p,q)=1
.
也就是说,问题相当于1<=p<=[k/g],1<=q<=[k/j]时,gcd(p,q)=1的对数.
因为p=q的时候只有(1,1)是合法的,我们可以先考虑p<q的时候有多少对(p,q),根据对称性,答案乘2就是p!=q时的对数,再加上p=q时的一个就好了.
而对于一个确定的q,p<q的时候合法的(p,q)对数相当于小于q且和q互质的数字个数.这应当都会算了.
于是对于一个确定的g,答案等于2*(phi[1]+phi[2]+phi[3]+...+phi[k/g])-1
,这个式子里我们把phi[1]放在括号里一起乘2,那么p=q时(1,1)被算了两次,所以要减一.
根据数论里常用的按除法结果分块
的技巧,不同的[k/g]的取值只有sqrt(k)个,枚举这sqrt(k)个取值,统计时乘上f(g,g)的区间和f(i,i)+f(i+1,i+1)+...+f(j-1,j-1)+f(j,j)
,比较显然的树状数组维护前缀和将需要O(msqrt(n)log(n))
的时间做查询.注意到修改操作比查询次数少到不知道哪里去了,我们可以利用分块,牺牲修改的复杂度,实现O(sqrt(n))修改,O(1)查询
.这个分块的细节看代码即可.还需要线性筛预处理欧拉函数前缀和,总的时间复杂度为O(n+m*sqrt(n))
那么我们来考虑另一个简化版本.
int ans=0;
for(g=1;g<=k;++g)
for(i=1;i<=k;++i)
for(j=1;j<=k;++j)
if(gcd(i,j)==g)ans+=f(g,g)*(i/g);
return ans;
//pseudocode,don't care about module or overflow
这里我们乘上了之前两个系数中的一个.按照和上一个简化版本相同的推导思路,(i,j)可以表示为(p*g,q*g),1<=p,q<=[k/g]
.如果满足条件,需要gcd(p,q)=1
.
也就是说,问题相当于1<=p<=[k/g],1<=q<=[k/j]时,gcd(p,q)=1的所有p之和
.因为p=q的时候只有(1,1)是合法的,我们可以先考虑p<q
的时候所有(p,q)的贡献之和,根据对称性,答案乘2就是p!=q时的贡献之和,再加上p=q时的1就好了.
而对于一个确定的q,p<q的时候合法的(p,q)的p之和相当于小于q且和q互质的数字之和.这个东西在q!=1的时候等于q*phi[q]/2
乘2之后,我们要算的是phi[1]*1+phi[2]*2+phi[3]*3+...+phi[k/g]*(k/g)
,这里(1,1)恰好只算了一次.
那么这个东西也可以预处理出前缀和,其他做法和上一个简化版本相同.
考虑原始问题.
int ans=0;
for(g=1;g<=k;++g)
for(i=1;i<=k;++i)
for(j=1;j<=k;++j)
if(gcd(i,j)==g)ans+=f(g,g)*(i/g)*(j/g);
return ans;
//pseudocode,don't care about module or overflow
沿着两个简化版本的思路,不难发现对于一个确定的q,p<q
的(p,q)对答案的贡献之和为小于q且和q互质的数字之和再乘q.这个东西在q!=1的时候等于q*q*phi[q]/2
,自然我们预处理i*i*phi[i]
的前缀和即可.其他做法和两个简化版本相同.细节详见代码.
#include<cstdio>
typedef long long ll;
const int mod=1000000007;
const int maxn=4000005;
int phi[maxn],prime[maxn],tot;
bool flag[maxn];
void linear(){
phi[1]=1;
for(int i=2;i<maxn;++i){
if(!flag[i]){
phi[i]=i-1;prime[++tot]=i;
}
for(int j=1;j<=tot&&prime[j]*i<maxn;++j){
flag[prime[j]*i]=true;
if(i%prime[j]==0){
phi[i*prime[j]]=phi[i]*prime[j];
break;
}else phi[i*prime[j]]=phi[i]*(prime[j]-1);
}
}
phi[1]=1;
for(int i=2;i<maxn;++i)phi[i]=phi[i]*1ll*i%mod*i%mod;
for(int i=1;i<maxn;++i)phi[i]=(phi[i-1]+phi[i])%mod;
}
int gcd(int a,int b){
return b==0?a:gcd(b,a%b);
}
const int sz=2000;
int fii[maxn],pre[maxn];
int belong[maxn],L[maxn],R[maxn];
int mark[maxn];
int totb;
void add(int x,int w){
int r=R[belong[x]];
for(int i=x;i<=r;++i){
pre[i]=(pre[i]+w)%mod;
}
for(int i=belong[x]+1;i<=totb;++i)mark[i]=(mark[i]+w)%mod;
}
int qpre(int x){
return (pre[x]+mark[belong[x]])%mod;
}
int work(int n){
ll ans=0;
ll cnt;
for(int i=1,last;i<=n;i=last+1){
last=n/(n/i);
cnt=phi[n/i];
ans=(ans+cnt*(qpre(last)-qpre(i-1)+mod)%mod);
}
return ans%mod;
}
int main(){
int m,n;scanf("%d%d",&m,&n);
linear();
int a,b,k;ll x;
for(int i=1;i<=n;++i)fii[i]=i*1ll*i%mod;
for(int i=1;i<=n;++i)pre[i]=(fii[i]+pre[i-1])%mod;
for(int i=1;i<=n;++i)belong[i]=(i-1)/sz+1;
totb=(n+sz-1)/sz;
for(int i=1;i<=totb;++i){
L[i]=(i-1)*sz+1;R[i]=i*sz;
}
R[totb]=n;
for(int i=1;i<=m;++i){
scanf("%d%d%lld%d",&a,&b,&x,&k);
int g=gcd(a,b);x=x/(a/g)/(b/g);x%=mod;
add(g,(x-fii[g]+mod)%mod);fii[g]=x;
printf("%d\n",work(k));
}
return 0;
}