[SDOI2015]约数个数和
[SDOI2015]约数个数和
闲话:莫比乌斯反演经典题
题目大意
求
\(T\) 组数据,且 \(1 \leq T,n,m \leq 50000\)
良心样例
输入样例
2
7 4
5 6
输出样例
110
121
一条有用的结论
小小的证明
对于一个 \(d|ij\) ,我们构造 \(x=\frac{i}{\gcd(i,d)} , y=\frac{d}{\gcd(i,d)}\)
现在我们就要找到 \(x,y\) 之间的关系,使我们能通过 \(x,y\) 得到 \(d\),而且能与所有的 \(d\) 一一对应
显然得到 \(\gcd(x,y)=1 , x|i\)
由\(y \gcd(i,j) | ij\)
得\(y | xj\)
又因为 \(\gcd(x,y)=1\)
所以 \(y|j\)
把所得的结论综合起来
\(\gcd(x,y)=1 , x|i , y|j\)
此时对于满足 \(\gcd(x,y)=1 , x|i , y|j\) 的 \(x,y\) 都可以得到 \(\frac{iy}{x}|ij\)
并且与所有的 \(d|ij\) 以一对应
也就是说用满足上述条件的 \(x,y\) 来构造 \(d\) ,能与所有的 \(d\) 一一对应
故这条有用的结论得证
正式切题
然后开始推式子
此时可以从两个方面入手,利用
或
化简式子
但无论如何,化简后的式子是一样的
所谓“殊途同归”
第一种推法
似乎已经有人注意到了
忽然出现了个 \(f\)
其实不必惊讶
我们发现后面那堆是因为这两个式子只和 \(\lfloor \frac nd \rfloor,\lfloor \frac md \rfloor\) 有关
故我们可以令 \(f(n)=\sum_{i=1}^n \lfloor \frac ni \rfloor\)
然后套用
发现一个问题
我们如何处理 \(f\)
你可以选择用到的时候再算 \(f\)
也可以 \(O(n \sqrt n)\) 预处理
如果选择前者就要注意了
计算过了的 \(f\) 就要记录下来,否则会超时
事实上
我的两种算 \(f\) 的方式在洛谷评测机前者跑得更快
尤其是在总时间上,快了差不多 \(2s\)
第二种推法
如旧有
设 \(f(d)=\sum_{i=1}^n \sum_{j=1}^m \sum_{x|i} \sum_{y|j} [\gcd(x,y)=d]\)
再设 \(F(d)=\sum_{d|k}f(k)=\sum_{i=1}^n \sum_{j=1}^m \sum_{x|i} \sum_{y|j} [d|\gcd(x,y)]\)
考虑反演
\(f(d)=\sum_{d|k}\mu(\frac kd)F(k)\)
而我们需要的答案是 \(f(1)\)
试试看
其实早在第二行的时候就“殊途同归”了!!
代码
f的第一中处理方式
#include<cstdio>
#include<iostream>
using namespace std;
typedef long long LL;
const int N = 50000;
int n , m , T , tot , vis[N + 5] , mu[N + 5] , prime[N + 5];
LL ans , f[N + 5];
inline void getm()
{
mu[1] = 1;
for(register int i = 2; i <= N; i++)
{
if (!vis[i]) prime[++tot] = i , mu[i] = -1;
for(register int j = 1; prime[j] * i <= N && j <= tot; j++)
{
vis[prime[j] * i] = 1;
if (i % prime[j] == 0) break;
mu[prime[j] * i] = -mu[i];
}
}
for(register int i = 2; i <= N; i++) mu[i] += mu[i - 1];
}
inline LL getf(int n)
{
int r;
LL res = 0;
for(register int l = 1; l <= n; l = r + 1)
{
r = n / (n / l);
res += 1LL * (r - l + 1) * (n / l);
}
return res;
}
int main()
{
getm();
for(register int i = 1; i <= N; i++) f[i] = -1;
scanf("%d" , &T);
while (T--)
{
scanf("%d%d" , &n , &m);
int r;
ans = 0;
for(register int l = 1; l <= min(n , m); l = r + 1)
{
r = min(n / (n / l) , m / (m / l));
if (f[n / l] == -1) f[n / l] = getf(n / l);
if (f[m / l] == -1) f[m / l] = getf(m / l);
ans += f[n / l] * f[m / l] * (mu[r] - mu[l - 1]);
}
printf("%lld\n" , ans);
}
}
f的第二种处理方式
#include<cstdio>
#include<iostream>
using namespace std;
typedef long long LL;
const int N = 50000;
int n , m , T , tot , vis[N + 5] , mu[N + 5] , prime[N + 5];
LL ans , f[N + 5];
inline void getm()
{
mu[1] = 1;
for(register int i = 2; i <= N; i++)
{
if (!vis[i]) prime[++tot] = i , mu[i] = -1;
for(register int j = 1; prime[j] * i <= N && j <= tot; j++)
{
vis[prime[j] * i] = 1;
if (i % prime[j] == 0) break;
mu[prime[j] * i] = -mu[i];
}
}
for(register int i = 2; i <= N; i++) mu[i] += mu[i - 1];
}
inline void getf()
{
int r;
for(register int i = 1; i <= N; i++)
for(register int l = 1; l <= i; l = r + 1)
{
r = i / (i / l);
f[i] += (r - l + 1) * (i / l);
}
}
int main()
{
getm() , getf();
scanf("%d" , &T);
while (T--)
{
scanf("%d%d" , &n , &m);
int r;
ans = 0;
for(register int l = 1; l <= min(n , m); l = r + 1)
{
r = min(n / (n / l) , m / (m / l));
ans += f[n / l] * f[m / l] * (mu[r] - mu[l - 1]);
}
printf("%lld\n" , ans);
}
}
实际上
关于处理 \(f\) 的办法有很多
再说一种
注意到
噢噢,其实 \(f\) 就是 \(\sigma_0\) 的前缀和
于是乎,线筛上
具体来说,约数个数定理不用白不用
线性筛时记录最小质因子的次数即可(因为只会被最小质因子筛到)。
此时预处理成了 \(O(n)\)
似乎还有杜教筛~~~