Luogu P1447 [NOI2010]能量采集
Preface
最近反演题做多了看什么都想反演。这道题由于数据弱,解法多种多样,这里简单分析一下。
首先转化下题目就是对于一个点\((x,y)\),所消耗的能量就是\(2(\gcd(x,y)-1)+1=2\cdot\gcd(x,y)-1\)(小学奥数题)
所以求和就是求\(\sum_{i=1}^n\sum_{j=1}^m2\cdot\gcd(i,j)-1=2\cdot\sum_{i=1}^n\sum_{j=1}^m\gcd(i,j)-nm\),因此主要问题就变成了求解\(\sum_{i=1}^n\sum_{j=1}^m\gcd(i,j)\)(对于反演来说,容斥的话直接怎么暴力怎么来)
一.转化+递推+容斥法
首先转化问题,我们考虑每一个数当它作为其它两个数的\(\gcd\)时会产生多少贡献。
先把问题简单化,我们先求出一个数为其它两个数的约数的方案数\(f(i)\),显然小学生都知道\(f(i)=\lfloor\frac{n}{i} \rfloor\lfloor\frac{m}{i} \rfloor\)
但是可能\(f(i)\)会多算当\(i|j\)时\(j\)的贡献,因此我们需要减掉\(\sum_{i|j} f(j)\)
因此我们从大到小枚举并计算\(f(i)\)即可,结合调和级数的公式发现复杂度为\(O(n(\ln(n)+H_n))\)(\(H_n\)是欧拉常数)
CODE
#include<cstdio>
#define RI register int
using namespace std;
int n,m,lim; long long ans,f[100005];
inline int min(int a,int b)
{
return a<b?a:b;
}
int main()
{
scanf("%d%d",&n,&m); lim=min(n,m);
for (RI i=lim;i;--i)
{
f[i]=1LL*(n/i)*(m/i);
for (RI j=i<<1;j<=lim;j+=i) f[i]-=f[j];
ans+=1LL*((i<<1LL)-1)*f[i];
}
return printf("%lld",ans),0;
}
二.我的蒟蒻反演做法
反演题做的多了就算是像我这样的蒟蒻也能自己推一推了。
感觉这个很套路就那以前的套路去套结果5min就搞出来一个看上去很正确的式子然后就过了。
这里讲一讲我的做法,根据之前做的一些反演题(如Luogu P2257 YY的GCD)我们很套路的来:
令\(f(d)\)为\(\gcd(i,j)(i\in[1,n],j\in[1,m])=d\)的个数,\(F(s)\)为\(d|\gcd(i,j)\)的个数
即:
考虑所求,则有:
\(ans=\sum_{d=1}^{\min(n,m)}f(d)\cdot d\)
还是用莫比乌斯反演定理,得到:
把\(F(s)=\lfloor\frac{n}{s} \rfloor \lfloor\frac{m}{s} \rfloor\)代进去有:
一个简单的套路,把枚举\(d\)变成\(n\)是\(d\)的多少倍,即:
再把这个代回去就有:
还要化简么?我们看看这道题的数据范围,\(n,m\le100000\),而且是单组询问
因此我们发现这个式子的复杂度根据调和级数定理为\(O(n(\ln(n)+H_n))\),因此暴力即可过
CODE
#include<cstdio>
#define RI register int
using namespace std;
const int P=100000;
int prime[P+5],cnt,phi[P+5],n,m,lim; bool vis[P+5]; long long ans,sum[P+5];
#define Pi prime[j]
inline void Euler(void)
{
vis[1]=phi[1]=1; RI i,j; for (i=2;i<=P;++i)
{
if (!vis[i]) prime[++cnt]=i,phi[i]=i-1;
for (RI j=1;j<=cnt&&i*Pi<=P;++j)
{
vis[i*Pi]=1; if (i%Pi) phi[i*Pi]=phi[i]*(Pi-1);
else { phi[i*Pi]=phi[i]*Pi; break; }
}
}
for (i=1;i<=P;++i) sum[i]=sum[i-1]+phi[i];
}
inline int min(int a,int b)
{
return a<b?a:b;
}
int main()
{
scanf("%d%d",&n,&m); Euler(); lim=min(n,m);
for (RI l=1,r;l<=lim;l=r+1)
{
r=min(n/(n/l),m/(m/l)); ans+=1LL*(n/l)*(m/l)*(sum[r]-sum[l-1]);
}
return printf("%lld",(ans<<1LL)-1LL*n*m),0;
}
三.利用狄利克雷卷积再加速
我们想一下,如果这题多组询问怎么办不保证毒瘤出题人不会出
因此我们思考应该还有更优秀的做法事实上也是有的
从第二种做法的\(f(d)=\sum_{d|s} \mu(\lfloor\frac{s}{d} \rfloor)\lfloor\frac{n}{s} \rfloor \lfloor\frac{m}{s} \rfloor\)开始,我们令\(T=ds\),则有:
由于经典的狄利克雷卷积我们知道\(\mu\)的一个性质:\(\sum_{d|n}d\cdot\mu(\lfloor\frac{n}{d} \rfloor)=\phi(n)\)
(这个貌似也可以用欧拉函数的性质\(\sum_{d|n}\phi(d)=n\)再反演得来反正我不会证)
所以到现在就很简单了,把这个代进去就有:
然后就是\(O(n)\)的式子了,当然,这个明显也可以做一发欧拉函数的前缀和结合除法分块做到单次询问\(O(\sqrt {\min(n,m))}\)的。因此多组询问也不再话下。
CODE
#include<cstdio>
#define RI register int
using namespace std;
const int P=100000;
int prime[P+5],cnt,phi[P+5],n,m,lim; bool vis[P+5]; long long ans,sum[P+5];
#define Pi prime[j]
inline void Euler(void)
{
vis[1]=phi[1]=1; RI i,j; for (i=2;i<=P;++i)
{
if (!vis[i]) prime[++cnt]=i,phi[i]=i-1;
for (RI j=1;j<=cnt&&i*Pi<=P;++j)
{
vis[i*Pi]=1; if (i%Pi) phi[i*Pi]=phi[i]*(Pi-1);
else { phi[i*Pi]=phi[i]*Pi; break; }
}
}
for (i=1;i<=P;++i) sum[i]=sum[i-1]+phi[i];
}
inline int min(int a,int b)
{
return a<b?a:b;
}
int main()
{
scanf("%d%d",&n,&m); Euler(); lim=min(n,m);
for (RI l=1,r;l<=lim;l=r+1)
{
r=min(n/(n/l),m/(m/l)); ans+=1LL*(n/l)*(m/l)*(sum[r]-sum[l-1]);
}
return printf("%lld",(ans<<1LL)-1LL*n*m),0;
}
Postscript
这真是一道很不错的数学题,上面的三种做法感觉都可以。
然而理论复杂度最优的算法三跑的和算法二一样。估计是除法做的太多常数偏大
而算法一就很优秀了,常数极小且不用欧拉筛的预处理,因此再速度上碾压了其它两种解法。
而且暴力不动脑的推导过程,啧啧称奇。