题解 luogu.P3601 签到题

题目

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,那么说明是个质数,就再做一次上面的操作。

总结归纳

数学题,多思考。这些细节非常有意思。同时,贡献观点也是非常有名,也非常有用的。

posted @ 2025-08-03 16:29  枯骨崖烟  阅读(9)  评论(0)    收藏  举报