欧拉筛和筛法求欧拉函数——杨子曰数学

欧拉筛和筛法求欧拉函数——杨子曰数学

超链接:数学合集


我们都知道,再这个世界上,有一个筛质数算法叫做埃筛,它的代码长这样:

void get_prime(int n){
	memset(flag,1,sizeof(flag));//flag开成bool就可以用memset赋值成1了
	flag[1]=0;
	for (int i=2;i<=n;i++){
		if (!flag[i]) continue;
		pr[++cnt]=i;
		for (int j=2;i*j<=n;j++){
			flag[i*j]=0;
		} 
	}
}

但是这种算法的太慢了(其实还好啦!),只有O(n log log n),不能满足我们的需求,今天给大家曰一种特别nb的筛素数的算法——欧拉筛,复杂度是O(n),优不优秀!!(你可能会说:也没比埃筛快多少呀!,在下面你会被它的用途,好处惊到)

先来思考一下,埃筛为什么会慢?因为它会把一个合数重复筛很多遍,比如12会被2筛一遍,会被3筛一遍,那么我们有没有什么办法让一个数只被筛一次捏?黑喂狗:


我们要做到的是一个合数只被它最小的质因数筛一次,So,我们需要用一个数组v[i],表示i的最小质因数,那么也就是说如果v[i]=i则说明i是一个质数,现在我们要想办法维护这个数组v

我们这样来操作:

  1. 把v数组全部赋成0
  2. 从2~n考虑每一个i
  3. 如果扫到当前v[i]=0,也就是还没有被比它小的任何一个质数更新到,说明这是一个质数,记录下来
  4. 对于i,我们扫描每个小于v[i]的质数p,那么对于i*p这个数,它的最小质因数一定是p,因为i的最小质因数一定大于等于p
    然后我们就完美地实现了欧拉筛

代码走起:

void get_prime(int n){
	memset(v,0,sizeof(v));
	for (int i=2;i<=n;i++){
		if (v[i]==0){
			v[i]=i;
			pr[++cnt]=i;
		}
		for (int j=1;j<=cnt;j++){
			if (pr[j]>v[i]||pr[j]>n/i) break;
			v[i*pr[j]]=pr[j];
		}
	} 
}

目前为止,肯定还有很多人没有体会到这个算法的优秀之处,且听我一一道来:

首先它可以用O(log n)分解质因数,我们已经有了每个数的最小质因数,那我们岂不是可以对当前的v[n]的指数++,然后n/=v[n],直到n变成1了为止,有木有发现不用枚举了!!美滋滋

void fac(int n){
	while(n>1){
		r[v[n]]++;
		n/=v[n];
	} 
}

如果你还没有被这个算法所折服,我再来告诉你,他可以O(n)筛法求出1~n的所有欧拉函数(什么?欧拉函数是什么?戳)

我们都知道欧拉函数的通向公式是:
φ ( n ) = n ∗ ( 1 − 1 p 1 ) ∗ ( 1 − 1 p 2 ) ∗ ( 1 − 1 p 3 ) ∗ ⋯ ∗ ( 1 − 1 p s ) \varphi(n)=n*(1-\frac{1}{p_1})*(1-\frac{1}{p_2})*(1-\frac{1}{p_3})* \cdots*(1-\frac{1}{p_s}) φ(n)=n(1p11)(1p21)(1p31)(1ps1)
如果一个数n它只比m多一个质因数p,也就是 n = m ∗ p n=m*p n=mp,且m没有质因数p,那我们是不是就可以用这个式子算出 φ ( n ) \varphi(n) φ(n)
φ ( n ) = φ ( m ) ∗ ( p − 1 ) \varphi(n)=\varphi(m)*(p-1) φ(n)=φ(m)(p1)
如果不理解的话,简单推导一下:
如 果 φ ( m ) = m ∗ ( 1 − 1 p 1 ) ∗ ( 1 − 1 p 2 ) ∗ ( 1 − 1 p 3 ) ∗ ⋯ ∗ ( 1 − 1 p s ) 如果\varphi(m)=m*(1-\frac{1}{p_1})*(1-\frac{1}{p_2})*(1-\frac{1}{p_3})* \cdots*(1-\frac{1}{p_s}) φ(m)=m(1p11)(1p21)(1p31)(1ps1)
那 么 φ ( n ) = m ∗ p ∗ ( 1 − 1 p 1 ) ∗ ( 1 − 1 p 2 ) ∗ ( 1 − 1 p 3 ) ∗ ⋯ ∗ ( 1 − 1 p s ) ∗ ( 1 − 1 p ) 那么\varphi(n)=m*p*(1-\frac{1}{p_1})*(1-\frac{1}{p_2})*(1-\frac{1}{p_3})* \cdots*(1-\frac{1}{p_s})*(1-\frac{1}{p}) φ(n)=mp(1p11)(1p21)(1p31)(1ps1)(1p1)
上面下面比较一下,你就会发现下面就比上面多乘了一个 p ∗ ( 1 − 1 p ) p*(1-\frac{1}{p}) p(1p1),就是 p − 1 p-1 p1

那么我们就可以用欧拉筛,在枚举小于v[i]的质数p,并更新 i ∗ p i*p ip时,有没有发现 i ∗ p i*p ip就是比i多了一个p,而且i没有p这个质因子(有可能有,因为当v[i]=p的时候我们也会去更新,等下在讲这个情况),于是乎,我们就可以用上面的方法更新了,也就是: φ ( i ∗ p ) = φ ( i ) ∗ ( p − 1 ) \varphi(i*p)=\varphi(i)*(p-1) φ(ip)=φ(i)(p1)

BUT,v[i]=p的时候我们也会去更新 i ∗ p i*p ip,但是这个时候 i ∗ p i*p ip和i就都有质因子i了,这时候,看上面两个式子,求 φ ( i ∗ p ) \varphi(i*p) φ(ip)时后面就不用多乘一个 ( 1 − 1 p ) (1-\frac{1}{p}) (1p1),但是前面要多乘一个p,在这种情况下,我们得到的是: φ ( i ∗ p ) = φ ( i ) ∗ p \varphi(i*p)=\varphi(i)*p φ(ip)=φ(i)p,判断一下即可

void get_phi(int n){
	phi[1]=1;
	memset(v,0,sizeof(v));
	for (int i=2;i<=n;i++){
		if (v[i]==0){
			v[i]=i;
			pr[++cnt]=i;
			phi[i]=i-1;
		}
		for (int j=1;j<=cnt;j++){
			if (pr[j]>v[i]||pr[j]>n/i) break;
			v[i*pr[j]]=pr[j];
			if (v[i]==pr[j]) phi[i*pr[j]]=phi[i]*pr[j];
			else phi[i*pr[j]]=phi[i]*(pr[j]-1);
		}
	} 
}

有没有瞬间觉得欧拉筛很nb!
——没有(逃

OK,完事

参考:《算法竞赛进阶指南》 李煜东 著

你可能感兴趣的:(变态的算法,崩溃的数学)