这里放传送门
记得当初懵(fei)懂(chang)无(sha)知(bi)的时候把这个题当一个无脑水题写了一发结果T的死惨死惨的。。。当时直接O(n)回答询问了也不想想能不能过。。然后现在这个题就变成了ATP的反演入门题_ (:з」∠)_
题目要求
设 F(k) 为满足 1≤i≤N,1≤j≤M 并且 gcd(i,j)=k 的数对个数, f(k) 为满足 1≤i≤N,1≤j≤M 并且公约数为 k ,即 k|gcd(i,j) 的数对个数,显然就有关系是 f(k)=∑k|dF(d) ,并且 f(k)=⌊Nk⌋⌊Mk⌋ 。
利用莫比乌斯反演公式可以得到 F(k)=∑k|dμ(dk)f(d) 。要求的实际上就是 ∑p为质数F(p) ,替换一下可以得到
化到这一步可以看出设 g(T)=∑p为质数,p|Tμ(Tp) ,如果能求出 g(T) 的前缀和,我们就可以用分块枚举除法的方法在 O(n√+m−−√) 的时间复杂度内回答每个询问。
求 g(T) 实际上有两个方法:第一个是枚举 1..N 范围内所有的质数,用类似埃式筛法的方法枚举每个质数的倍数来更新,这样的复杂度是 O(nloglogn) 的;还有更快一点的方法是用线性筛。
用线性筛求一个函数一般分三步:
这样的话我们就能用 O(n+T(n√+m−−√)) 的复杂度解决这个题了。
实际上ATP一开始不是这么做的。。仍然用了愚蠢的xjb推式子方法。。
如果有人有兴趣的话就溜一眼好了。。
原始式子为
这样就用了一种极其麻烦的方法得到了和上面一样的结论。。
#include
#include
#include
using namespace std;
int T,n,m,prm[10000010],mu[10000010],tail;
long long ans,sum[10000010];
bool ext[10000010];
long long Mul(long long x,long long y){return x*y;}
void get_prime(int N){
mu[1]=1;
for (int i=2;i<=N;i++){
if (ext[i]==false){
prm[++prm[0]]=i;
mu[i]=-1;sum[i]=1;
}
for (int j=1;j<=prm[0];j++){
if ((long long)i*prm[j]>N) break;
ext[i*prm[j]]=true;
if (i%prm[j]==0){
sum[i*prm[j]]=mu[i];
break;
}
else{
mu[i*prm[j]]=-mu[i];
sum[i*prm[j]]=mu[i]-sum[i];
}
}
}
for (int i=1;i<=N;i++) sum[i]+=sum[i-1];
}
int main()
{
scanf("%d",&T);
get_prime(10000000);
for (int wer=1;wer<=T;wer++){
scanf("%d%d",&n,&m);
if (n>m) swap(n,m);
ans=0;
for (int i=1;i<=n;i=tail+1){
tail=min(n,min(n/(n/i),m/(m/i)));
ans=ans+Mul(n/i,m/i)*(sum[tail]-sum[i-1]);
}
printf("%I64d\n",ans);
}
return 0;
}
如果科学地设出一个函数,用这个函数来做反演,就比xjb推式子要简单很多。。
如果出现了类似 Ni∗d 这样的东西,考虑一下能不能把 i∗d 看成一个整体,从枚举倍数转为枚举因数。这样往往可以构造出能够线筛的函数,这也是很多反演题目的常见思路。
如果发现题目有好多化不掉的 ∑ ,试试能不能把里面的东西xjb乱移动一下,把能够一起算的东西凑到一起去,这样有时候可以做到把嵌套的循环变成并列的循环。
线性筛三步走的思路非常常用,但是有些时候也不仅仅局限于这个思路,还是要观察要筛的函数本身的性质。