poj2478题解-欧拉函数(Farey Sequence)

poj2478题解-欧拉函数

前言

先点关注,不迷路

大家好,我是于斯为盛
这一段刚好在学欧拉函数,说实话刚看到这道题的时候我还是有点懵的
通过这道题,我也意识到了define的重要性
哎,两个地方少打了零
废话不多说
原题传送门: 点这里.
讲得不好勿喷~

暴力方法

暴力方法我想很多人都能想明白
无非就是三层循环

	while 输入n
		for 循环分母
			for 循环分子

最多加个前缀和,打个表的优化

	for 循环分母
		for 循环分子//打表
	while 输入,从表中读取

但是…
O(n2),n=106
也就是最少1012
或许可以找个超算中心

所以暴力方法有什么用呢?

我认为,虽然不能过
但是日后调试也是有大用处的

欧拉函数优化

欧拉函数不知道大家知不知道
φ(m)表示m以内与m互素的数的个数

题目里要求分母在n以内且分子整除分母且小于分母的个数
可以转化为Σφ(i)

然后这里有一个求φ的方法
设:m=p1^e1 * p2^e2……(唯一分解定理,pi为素数)
则φ(m)=m*(1-1/p1)*(1-1/p2)……
证明的话点这里

但是这里注意到1/pi是个分数
我们不妨把上式通分
得到φ(m)=m*((p1-1)/p1 )*((p2-1)/p2 )……
由于pi|m
在除的时候不会出现小数

还有要注意一点,在循环pi的时候不是单纯的看是不是整除
而还要做一个素数筛法
我这里用的埃氏筛法

	while 输入n
		for i循环1~n,求φ(m),累和
			for m循环1~根号i(大于根号i的素数因子最多有一个,后面单独处理即可)
				for k循环m, k*m<=根号i(埃氏筛法找合数)
				//因为当k<m时,k*m已经被前面的素数筛掉了,所以从m*m开始
				//当k*m大于根号i时,后面的数反正单独处理,筛它也没用

但是这样也没有太大的优化效果

打表+欧拉函数优化

打表前文已经提过了
把求φ的过程提到前面

```c
	for i循环1~n,求φ(m),累和
		for m循环1~根号i(大于根号i的素数因子最多有一个,后面单独处理即可)
			for k循环m, k*m<=根号i(埃氏筛法找合数)
			//因为当k<m时,k*m已经被前面的素数筛掉了,所以从m*m开始
	while 输入n

这样,我们把复杂度提到了O(n* 根号n* 根号n)甚至还要小
等等!!!没完呢!!!
我先把代码粘一下
但是可能有人发现了
O(n* 根号n* 根号n)当n=106时复杂度还是差点

//code by 于斯为盛
#include <iostream>

using namespace std;

long long _fai[1000005];//φ,应该是phi~后文为了省事,变成了存前缀和的数组
//ps:十年OI一场空,不开long long见祖宗
bool _use[1005];//素数表,我懒的取名字了
int _n;

void GenFai(int p, int cur){//这个真是求φ的p表示当前数组位置,cur就是前文的m,这俩实际上是一个
	
	for(int i=1; i<=1000; i++){
		_use[i]=false;//由于反复用,这里需要初始化
		/*
		好吧,这里其实没必要的
		完全可以把素数筛法提到外面单独处理
		但是你可以往后看
		即使省了筛素数的根号n
		也是不够的
		因为这样也只减了一个远不到根号n的复杂度
		*/
	}
	
	_fai[p]=cur;
	int curM=cur;
	for(int i=2; i*i<=cur; i++){
		
		if(_use[i]){
			continue;//是合数
		}
		
		if(curM%i==0){
			_fai[p]/=i;
			_fai[p]*=i-1;//即*((pi-1)/pi)
			
			curM/=i;
		
			while(curM%i==0){
				curM/=i;
			}
		}
		
		if(curM==1){
			break;//求完了,就可以退出了
		}

		for(int m=i; m*i<=1005; m++){
			_use[i*m]=true;
		}
	}
	
	if(curM!=1){	
		_fai[p]/=curM;
		_fai[p]*=curM-1;//特判剩下大于根号i的素数
	}
}

void GenList(){
	_fai[2]=1;
	for(int i=3; i<=1000005; i++){
		GenFai(i,i);
		_fai[i]+=_fai[i-1];//前缀和
	}
}

int main(){
	int cur;
	GenList();
	while(cin>>cur){
		if(cur==0){
			return 0;
		}
		cout<<_fai[cur];
	}
	return 0;
}

当素数筛法和欧拉函数一起搞

在筛素数的地方一起循环,直接把能整除当前的素数的φ给乘上((pi-1) /pi
这样就是类似一个递推的过程了(不完全是)
从循环m,看哪个数能整除m
变成了循环i,看哪个数能被i整除
也是一个逆向的过程
也就是这样

for i 1~n
	if 合数 退出
	for m = 2, m*i<=n
		phi[i*m]*=((pi-1) /pi)
	if i<=根号n
		for m = i, m*i<=n
			把m*i标记为合数

虽然还是O(n2
但是越往后合数越密集,所以后一个n几乎可以忽略
这样就可以卡边过了

说到这里可能会有人有疑问了
那前一个方法忽略了根号n行不行呢?
不行
实际上前一个方法的合数退出判断是在前面n* 根号n的后面
n* 根号n当n=106时也是过不了的

好了,上AC代码!!!

//code by 于斯为盛
#include <iostream>

using namespace std;

long long _fai[1000005];
bool _use[1000005];
int _n;

void GenFai(){
	
	for(int i = 1; i <= 1000000; i++){
		_use[i] = false;
		_fai[i] = i; 
	}
	
	for(int i = 2; i <= 1000000; i++){
		if(_use[i]) {
			continue;
		}
		
		for(int m = 1; m*i <= 1000000; m++){//直接处理能被i整除的数
			_fai[m*i]/=i;
			_fai[m*i]*=i-1;
		}
		
		if(i<=1000){//这里其实就是i<=根号n
		
			for(int m = i; i*m <= 1000000; m++){
				_use[i*m] = true;
			}
		}
	}
}

void GenList(){
	GenFai();
	for(int i=3; i<=1000005; i++){
		_fai[i]+=_fai[i-1];
	}
}

int main(){
	int cur;
	GenList();
	while(cin>>cur){
		if(cur==0){
			return 0;
		}
		cout<<_fai[cur]<<endl;
	}
	return 0;
}

总结

回顾前面的方法
除了暴力以外,我们的优化实际上都是在尽量减少重复运算
打表不用重复算欧拉函数
循环看被整除的数也是降低重复访问
在遇到爆T时,大家除了换算法以外也可以想想减少重复运算

另外,大家不要忘了点个关注呀~
欧拉函数的证明那块我会在下一篇博客详细讲

好了,差不多说完了
如果有 错误/优化/疑问 欢迎提出
WeChat:wxid_ffe28hxx677f32
(其实是我想认识大佬)
——by 于斯为盛

视频讲解欧拉定理: 点这里

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:1024 设计师:我叫白小胖 返回首页
评论 2

打赏作者

于斯为盛

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值