题解 luogu.P3601 签到题
题目
一道非常有细节和思维含量但是非常优美的数学题。
题意建模
很简单,给定一个区间,求该区间内每个数与其欧拉函数之差的总和。即:
我们定义一个函数:\(\operatorname{qiandao}(x)\) 为小于等于 \(x\) 的数中,与 \(x\) 不互质的数的个数。
这题作为签到题,给出 \(l\) 和 \(r\),求出:
\[\sum_{i=l}^r \operatorname{qiandao}(i)\bmod 666623333
\]
题意极简,但是数据范围极大:
- 对于 \(30\%\) 的数据,\(l,r\leq 10^3\)。
- 对于 \(60\%\) 的数据,\(l,r\leq 10^7\)。
- 对于 \(100\%\) 的数据,\(1 \leq l \leq r \leq 10^{12}\),\(r-l \leq 10^6\)。
算法分析
首先,暴力只能得到 \(60pts\),也即是对应着第二的数据范围。我们考虑一下如何求解?
数据范围过大,所以试除法不行。但是在学习试除法的同时,我们积累了一个经验:逆向思维,考虑每个数的贡献,也即贡献观点(笔者起的名字)。
具体而言,本题可以考虑每个在 \([1,10^{6}]\) 的数的贡献,提前筛出这些质数,然后去更新 \([l,r]\) 区间内的数。这是本题的大致思路。
我们对比一下两份代码,并在细节研讨板块详细解释一下代码的细节。
参考代码
暴力法
#include<iostream>
#include<cstring>
#define int long long
using namespace std;
const int N=1e7+5,mod=666623333;
int pri[N],idx,phi[N];
bool is_pri[N];
void get_phi(int n)
{
memset(is_pri,true,sizeof(is_pri));
is_pri[1]=false;
for(int i=2;i<=n;i++)
{
if(is_pri[i]) pri[++idx]=i,phi[i]=i-1;//i是质数
for(int j=1;i*pri[j]<=n;j++)
{
int temp=i*pri[j];
is_pri[temp]=false;//temp不是质数
if(i%pri[j]==0) //pri[j]整除i
{
phi[temp]=phi[i]*pri[j];
break;
}
else phi[temp]=phi[i]*(pri[j]-1);
}
}
}
signed main()
{
int l,r; cin>>l>>r;
get_phi(r);
int ans=0;
for(int i=l;i<=r;i++) (ans+=i-phi[i])%=mod;
cout<<ans<<endl;
return 0;
}
正解
#include<iostream>
#include<cstring>
#include<cmath>
#define int long long
using namespace std;
const int N=1e6+5,mod=666623333;
int pri[N],idx,phi[N],temp[N];
bool is_pri[N];
int l,r;
void get_pri(int n)
{
memset(is_pri,true,sizeof(is_pri));
is_pri[1]=false;
for(int i=2;i<=n;i++)
{
if(is_pri[i]) pri[++idx]=i;
for(int j=1;i*pri[j]<=n;j++)
{
is_pri[i*pri[j]]=false;
if(i%pri[j]==0) break;
}
}
}
int solve()
{
int res=0;
for(int i=l;i<=r;i++) temp[i-l]=phi[i-l]=i;
for(int i=1;i<=idx;i++)
for(int j=((l-1)/pri[i]+1)*pri[i]; j<=r; j+=pri[i])
if(temp[j-l]%pri[i]==0)
{
phi[j-l]=phi[j-l]/pri[i]*(pri[i]-1);
while(temp[j-l]%pri[i]==0) temp[j-l]/=pri[i];
}
for(int i=l;i<=r;i++)
if(temp[i-l]!=1)
phi[i-l]=phi[i-l]/temp[i-l]*(temp[i-l]-1);
for(int i=l;i<=r;i++)
(res+=i-phi[i-l])%=mod;
return res;
}
signed main()
{
cin>>l>>r;
get_pri(ceil(sqrt(r)));
cout<<solve()<<endl;
return 0;
}
细节实现
我们的重点放在solve()函数的实现上。
-
for(int i=l;i<=r;i++) temp[i-l]=phi[i-l]=i;
这一句,是将下标映射到内存空间的范围内; -
for(int i=1;i<=idx;i++) for(int j=((l-1)/pri[i]+1)*pri[i]; j<=r; j+=pri[i])
外层循环枚举每一个在 \([1,\sqrt{r}]\) 上的质数,内层循环枚举每一个在 \([l,r]\) 上的已经预处理出来质数的倍数
if(temp[j-l]%pri[i]==0)
{
phi[j-l]=phi[j-l]/pri[i]*(pri[i]-1);
while(temp[j-l]%pri[i]==0) temp[j-l]/=pri[i];
}
- 这一段,很关键。只要是因数,就由算术基本定理将其分解掉。基于以下这个性质:
$\varphi(x)= \left\lfloor \frac{\varphi(x)}{pri_{i}} \right\rfloor \times pri_{i} $
怎么理解?这个数有一个质因子,那就不断将其分出去,然后有质因子欧拉函数的定义,可以化为常数。
for(int i=l;i<=r;i++)
if(temp[i-l]!=1)
phi[i-l]=phi[i-l]/temp[i-l]*(temp[i-l]-1);
- 这一段,就是经过上面的循环之后,如果最终的结果不为1,那么说明是个质数,就再做一次上面的操作。
总结归纳
数学题,多思考。这些细节非常有意思。同时,贡献观点也是非常有名,也非常有用的。

浙公网安备 33010602011771号