算法学习笔记() 数论之素数判断和计数——埃式筛、欧拉筛、区间素数、素因子分解、欧拉与Mobius函数

本文介绍了高级算法中的素数测试(包括试除法和Miller-Rabin测试)和素数计数(如埃利斯塔特筛法与欧拉筛法),并探讨了如何在大数据范围内应用这些方法,以及素因子分解、欧拉函数和Mobius函数的计算。通过实例和代码展示,深入解析算法背后的原理和优化技巧。

本文属于「算法学习」系列文章的汇总目录。之前的「数据结构和算法设计」系列着重于基础的数据结构和算法设计课程的学习,与之不同的是,这一系列主要用来记录大学课程范围之外的高级算法学习、优化与应用的全过程,同时也将归纳总结出简洁明了的算法模板,以便记忆和运用。在本系列学习文章中,为了透彻理解算法和代码,本人参考了诸多博客、教程、文档、书籍等资料,由于精力有限,恕不能一一列出,这里只列示重要资料的不完全参考列表:


为了方便在PC上运行调试、分享代码,我还建立了相关的仓库。在这一仓库中,你可以看到算法文章、模板代码、应用题目等等。由于本系列文章的内容随时可能发生更新变动,欢迎关注和收藏算法学习系列文章目录一文以作备忘。


素数的定义和性质见【数论】第1章 整数的可除性(1) 整除概念与带余除法(2) 素数。本文中我们将讨论,如何用算法求解两个关于素数的问题——素性测试和素数计数(筛法)。

1. 素性测试

问题很简单:输入一个很大的正整数 n n n ,判断它是不是素数。

1.1 用试除法判断素数

根据素数的定义(定义1.1.2),一个数 n n n ,如果不能被 [ 2 , n − 1 ] [2, n - 1] [2,n1] 内的所有整数整除, n n n 就是素数。当然,我们不需要把 [ 2 , n − 1 ] [2, n - 1] [2,n1] 内的数都试一遍,这个范围可以缩小到 [ 2 , n ] [2, \sqrt{n}] [2,n ]

给定 n n n ,如果它不能被 [ 2 , n ] [2, \sqrt{n}] [2,n ] 内的所有数整除,它就是素数(证明见定理1.1.1)。因此,我们只要检查 [ 2 , n ] [2, \sqrt{n}] [2,n ] 内的数,如果 n n n 不是素数,就一定能找到一个数 a   ( 2 ≤ a ≤ n ) a\ (2 \le a \le \sqrt{n}) a (2an ) 整除 n n n ;如果不存在这样的 a a a ,那么 [ n , n − 1 ] [\sqrt{n}, n - 1] [n ,n1] 中也不存在 b   ( n ≤ b < n ) b\ ( \sqrt{n} \le b < n) b (n b<n) 整除 n n n

后面的讲解中,会进一步缩小以上判断的范围: [ 2 , n ] [2, \sqrt{n}] [2,n ] 内所有的素数。原理很简单——如果 n n n 是合数,则 n n n 一定能被 [ 2 , n ] [2, \sqrt{n}] [2,n ] 内的某个数 y y y 整除:

  • y y y 是素数时,就不讨论了;
  • y y y 是合数时,则 y y y 一定能被 [ 2 , y ] [2, \sqrt{y}] [2,y ] 内的某个数整除,如此递归,最终必然对应到一个比它小的素数 x x x 、能被 x x x 整除。

用试除法判断素数,复杂度是 O ( n ) O(\sqrt{n}) O(n ) ,对于 n ≤ 1 0 12 n \le 10^{12} n1012 的数是没有问题的。下面是试除法判断素数的模板代码

bool isPrime(int n) {
	if (n <= 1) return false;	// 1不是素数
	if (n == 2) return true;	// 2是素数
	if (n % 2 == 0) return false; // 除2以外的正偶数都不是素数
	int sqr = sqrt(n);
	for (int i = 2; i <= sqr; ++i) // 也可以写成 for(int i = 2; i * i <= n; ++i)
		if (n % i == 0) return false; // 能整除,不是素数
	return true;
}

1.2 巨大素数的判断——Miller-Rabin 测试

如果 n n n 非常大,例如POJ 1811题 1 ≤ n < 2 54 1 \le n < 2^{54} 1n<254 ,判断 n n n 是不是素数。如果用试除法, n = 2 27 ≈ 1 0 8 \sqrt{n} = 2^{27} \approx 10^8 n =227108 ,复杂度仍然太高。此时需要用到特殊而复杂的方法,见 《ACM/ICPC算法训练教程》P127页 余立功 清华大学出版社

要测试 n n n 是否为素数,首先将 n − 1 n - 1 n1 分解为 2 s d 2^sd 2sd 。每次测试开始时,随机选一个 [ 1 , n − 1 ] [1, n - 1] [1,n1] 的整数 a a a ,如果对所有的 r ∈ [ 0 , s − 1 ] r \in [0, s- 1] r[0,s1] ,都满足 a d   m o d   n ≠ 1 a^d \bmod n \ne 1 admodn=1 a 2 r d   m o d   n ≠ − 1 a^{2^rd}\bmod n \ne -1 a2rdmodn=1 ,则 n n n 是合数。否则, n n n 3 4 \dfrac{3}{4} 43 的记录为素数。为了提高测试的正确性,可选择不同的 a a a 进行多次测试。

Miller-Rabin 素性测试的模板代码如下:

bool test(int n, int a, int d) {
	if (n == 2) return true; // 2是素数
	if (n == a) return true; // 选择的a也是素数
	if ((n & 1) == 0) return false; // 非2的偶数不是素数
	while (!(d & 1)) d = d >> 1;
	int t = fastPow(a, d, n); // 快速幂,计算a^d % n
	while ((d != n - 1) && (t != 1) && (t != n - 1)) {
		t = (long long)t * t % n;
		d = d << 1;
	}
	return (t == n - 1 || (d & 1) == 1);
}

bool isPrime(int n) { // false表示n为合数,true表示n有很大几率为素数
	if (n < 2) return false;
	int a[] = {2, 3, 61}; // 测试集,更大的测试范围需要更大的测试集
	for (int i = 0; i <= 2; ++i)
		if (!test(n, a[i], n - 1)) return false;
	return true;
}

算法的时间复杂度为 O ( log ⁡ n ) O(\log n) O(logn)


2. 素数筛法与计数

一个与素数相关的问题是求 [ 2 , n ] [2, n] [2,n] 内所有的素数。如果用上面的试除法,一个个单独判断,太慢了。

2.1 埃利特斯拉筛法

埃式筛法是一种古老而有效的方法,可以快速找到 [ 2 , n ] [2, n] [2,n] 内所有的素。对于初始队列 { 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , … , n } \{ 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, \dots, n\} {2,3,4,5,6,7,8,9,10,11,12,13,,n} ,操作步骤如下:
(1)输出最小的素数 2 2 2 ,然后筛掉 2 2 2 的倍数,剩下 { 3 , 5 , 7 , 9 , 11 , 13 , …   } \{ 3, 5, 7, 9, 11, 13, \dots \} {3,5,7,9,11,13,}
(2)输出最小的素数 3 3 3 ,然后筛掉 3 3 3 的倍数,剩下 { 5 , 7 , 11 , 13 , …   } \{ 5, 7, 11, 13, \dots \} {5,7,11,13,}
(3)输出最小的素数 5 5 5 ,然后筛掉 5 5 5 的倍数,剩下 { 7 , 11 , 13 , …   } \{ 7, 11, 13, \dots \} {7,11,13,}
(4)继续以上步骤,直到队列为空。

下面是朴素的埃式筛法模板代码。其中 inp[i]is not prime)记录数 i i i 的状态,如果 inp[i] = true ,表示它被筛掉了、不是素数。用 primes 数组存放素数,例如 prime[0] 是第一个素数 2 2 2

const int maxn = 1e7; 				// 定义空间大小,1e7约1.25MB
vector<int> primes;					// 存放素数
bitset<maxn> inp;					// inp[i]=true,表示i不是素数
int E_sieve(int n) {				// 埃式筛法,计算[2,n]内的素数
	for (int i = 2; i <= n; ++i) {	// 从第1个素数2开始,可优化(1)
		if (inp[i] == false) { 		// i是素数
			primes.push_back(i); 	// 保存到primes中
			for (int j = i * 2; j <= n; j += i) // i的倍数都不是素数,可优化(2)
				inp[j] = true;
		}
	}
	return primes.size();			// 统计素数的个数
}

时间复杂度: 2 2 2 的倍数被筛掉,计算 n / 2 n / 2 n/2 次; 3 3 3 的倍数被筛掉,计算 n / 3 n / 3 n/3 次; 5 5 5 的倍数被筛掉,计算 n / 5 n / 5 n/5 次。依次类推,总次数是 O ( n / 2 + n / 3 + n / 5 + …   ) O(n / 2 + n / 3+ n / 5 + \dots ) O(n/2+n/3+n/5+) ,这里直接给出结果,即 O ( n log ⁡ log ⁡ 2 n ) O(n \log \log_2n) O(nloglog2n)
空间复杂度:程序用到了位图 bitset<maxn> inp ,当范围达到 1 0 7 10^7 107 时约为1.25MB,再加上 primes ,空间不会超过10MB。一般题目会限制空间为65MB。

上述代码有两处可以优化:

  • 用来做筛除的数为 2 , 3 , 5 2, 3, 5 2,3,5 等,最多到 n \sqrt{n} n 就可以了,其原理和试除法一样——合数 k   ( ≤ n ) k\ (\le n) k (n) 必定可以被一个小于等于 k   ( ≤ n ) \sqrt{k}\ ( \le \sqrt{n}) k  (n ) 的素数整除(见埃利特斯拉筛法的相关说明)。例如,求 n = 100 n = 100 n=100 以内的素数,用 2 , 3 , 5 , 7 2, 3, 5, 7 2,3,5,7 筛就足够了。注意,此时如果要存储素数,还需要一次额外的for循环
  • for (int j = i * 2; j <= n; j += i) 中的 j = 2 * i ,可优化为 j = i * i减少重复筛数,注意当 n ≥ 1 0 6 n \ge 10^6 n106 时,i * i 可能溢出,需要强制类型转换。例如 i = 5 i = 5 i=5 时, 2 × 5 ,   3 × 5 ,   4 × 5 2 \times 5,\ 3 \times 5, \ 4 \times 5 2×5, 3×5, 4×5 早已在前面 i = 2 , 3 , 4 i = 2, 3, 4 i=2,3,4 的时候筛过了。

优化后的埃式筛法模板代码如下:

const int maxn = 1e7; 				// 定义空间大小,1e7约1.25MB
vector<int> primes;					// 存放素数
bitset<maxn> inp;					// inp[i]=true,表示i不是素数
int E_sieve(int n) {				// 埃式筛法,计算[2,n]内的素数
	int sqr = sqrt(n);
	for (int i = 2; i <= sqr; ++i)  // 从第1个素数2开始,可写成i*i<=n
		if (inp[i] == false)  		// i是素数
			for (int j = i * i; j <= n; j += i)
				inp[j] = true;
	for (int i = 2; i <= n; ++i)
		if (inp[i] == false)
			primes.push_back(i); 	// 保存到primes中
	return primes.size();			// 统计素数的个数
}

埃式筛法还不错,但还是做了一些无用功,某些合数会被筛几次,比如 12 12 12 会被 2 2 2 3 3 3 筛两次。不过,埃式筛法可以近似地看做 O ( n ) O(n) O(n) 的,一般也够用了。只是在一些数据范围达到 1e7 的题目中,难以让人满意,下面就介绍欧拉筛法,时间复杂度仅为 O ( n ) O(n) O(n) 的线性筛法。

2.2 欧拉筛法

如何确保每个合数只被筛选一次呢?我们只要用它的最小质因子来筛选即可,显然,每个数的最小质因子只有一个、只能被筛一次。这就是欧拉筛——在埃氏筛法的基础上,让每个合数只被它的最小质因子筛选一次,以达到不重复筛数的目的

我们直接看欧拉筛的模板代码

const int maxn = 1e7; 				// 定义空间大小,1e7约1.25MB
vector<int> primes;					// 存放素数
bitset<maxn> inp;					// inp[i]=true,表示i不是素数
int Euler_sieve(int n) {			// 欧拉筛法,计算[2,n]内的素数
	for (int i = 2; i <= n; ++i) {  // 从第1个素数2开始
		if (inp[i] == false)
			primes.push_back(i); 	// 保存到primes中
		for (int j = 1; j < primes.size(); ++j) {
			if (i * primes[j] <= n) break; // 超出要求的范围,退出
			inp[i * primes[j]] = true;	   // 解释
			if (i % primes[j] == 0) break; // 解释
		}
	}
	return primes.size();			// 统计素数的个数
}

特别地,对于 inp[i * primes[j]] = true 的解释:这里不是用 i 的倍数消去合数,而是primes 里面记录的素数,从小到大来当做消去合数的最小质因子。打表观察来理解,可见是用最小质因子 primes[j]i 倍数来筛去合数,且筛去的合数没有重复

对于 i % primes[j] == 0 就跳出循环的解释——这是避免重复筛数的关键所在,此时显然有 primes[i] | i ,即有 i = primes[j] * q ,如果继续循环到 j + 1i * primes[j + 1] = primes[j] * q * primes[j + 1] ,注意到这里的 primes[j] 才是最小质因子,应该由 primes[j]i = q * primes[j + 1] 时筛去,而不是由 primes[j + 1]i = primes[j] * q 时筛去,所以要跳出循环。

举例说明,当 i = 8, j = 1, primes[j] = 2 时,如果不跳出循环,prime[j + 1] = 3, 8 * 3 = 24 = (2 * 4) * 3 = 2 * (4 * 3) ,会筛去 24 ——实际上,24 应在 i = 12 时筛去、而不是在 i = 8 时筛去。

2.3 筛法应用于小区间大素数筛选

用埃式/欧拉筛法求 [ 2 , n ] [2, n] [2,n] 内的素数,能解决 n ≤ 1 0 7 n \le 10^7 n107 的问题。如果 n n n 更大,某些情况下还是可以用筛法来处理,这就是小区间大素数的计算(小的区间、大的素数)——把 [ 2 , n ] [2, n] [2,n] 看做一个区间,然后把筛法扩展到求区间 [ a , b ] [a, b] [a,b] 的素数, a < b ≤ 1 0 12 ,   b − a ≤ 1 0 6 a < b \le 10^{12},\ b - a \le 10^6 a<b1012, ba106

前文提到,用试除法判断 n n n 是否为素数,更深的原理为:如果它不能被 [ 2 , n ] [2, \sqrt{n}] [2,n ] 内的所有素数整除,它就是素数。容易理解这个原理: [ 2 , n ] [2, \sqrt{n}] [2,n ] 内的非素数 y y y ,肯定对应一个比它小的素数 x x x 。在用试除法时,如果 n n n 能被 x x x 整除,则证明了 n n n 不是素数,就不用在试 y y y 了。

这个原理可以和筛法结合,用来解决大区间求素数的问题。具体来说,先用埃式/欧拉筛法求 [ 2 , b ] [2, \sqrt{b}] [2,b ] 内的素数,然后用这些素数来筛选 [ a , b ] [a, b] [a,b] 区间的素数——对每个 [ a , b ] [a, b] [a,b] 内的数,如果它是合数,则必定被 [ 2 , b ] [2, \sqrt{b}] [2,b ] 内的某个素数整除。
(1)时间复杂度:使用埃式筛法,则为 O ( b log ⁡ log ⁡ b ) + O ( ( b − a ) b − a ) O(\sqrt{b} \log \log \sqrt{b}) + O\big((b - a) \sqrt{b - a}\big) O(b loglogb )+O((ba)ba ) ;使用欧拉筛法,则为 O ( b ) + O ( ( b − a ) b − a ) O(\sqrt{b}) + O\big((b - a) \sqrt{b - a}\big) O(b )+O((ba)ba ) ???
(2)空间复杂度:需要定义两个数组,一个用来处理 [ 2 , b ] [2, \sqrt{b}] [2,b ] 内素数,另一个用于处理 [ a , b ] [a, b] [a,b] 内的素数,空间复杂度为 O ( b ) + O ( b − a ) O(\sqrt{b}) + O(b - a) O(b )+O(ba)

2.4 更大的素数——大区间素数计算

如果要统计更大范围内的素数个数,例如 n = 1 0 11 n = 10^{11} n=1011 时有 40 40 40 多亿个素数([2, n]内素数的数量),就要用到更加复杂的数学方法。


3. 素因子分解

n n n 的质因数要么是 n n n 本身( n n n 是质数)、要么一定小于等于 n \sqrt{n} n 。因此可用小于等于 n \sqrt{n} n 的数对 n n n 进行试除,一直到不能除为止。这时候剩下的数如果不是 1 1 1 ,那么就是 n n n 最大的质因数。

质因数分解的模板代码如下,时间复杂度为 O ( n ) O(\sqrt{n}) O(n )

vector<int> pfac; // 存储质因数
vector<int> fexp; // 存储质因数对应的指数
void factor(int n) {
	if (n <= 1) return;
	int sqr = sqrt(n), now = n;
	for (int i = 2; i <= sqr; ++i) {
		if (now % i == 0) {
			pfac.push_back(i);
			int cnt = 0;
			while (now % i == 0) {
				++cnt;
				now /= i;
			}
			fexp.push_back(cnt);
		}
	}
	if (now != 1) {
		pfac.push_back(now);
		fexp.push_back(1);
	}
}

4. 欧拉函数计算

计算 n n n 的欧拉函数 Φ ( n ) \varPhi(n) Φ(n) ——其定义表示,小于等于 n n n 的数中与 n n n 互质的数的数目。

欧拉函数求值的方法是:
(1) Φ ( 1 ) = 1 \varPhi(1) = 1 Φ(1)=1
(2)若 n n n 是素数 p p p k k k 次幂,则 Φ ( n ) = p k − p k − 1 = ( p − 1 ) p k − 1 \varPhi(n) = p^k - p^{k - 1} = (p - 1)p^{k - 1} Φ(n)=pkpk1=(p1)pk1
(3)若 m , n m, n m,n 互质,则 Φ ( m n ) = Φ ( m ) Φ ( n ) \varPhi(mn) = \varPhi(m) \varPhi(n) Φ(mn)=Φ(m)Φ(n)

根据欧拉函数的定义和性质,可以推出欧拉函数的递推式:令 p p p n n n 的最小质因数,若 p 2 ∣ n p^2 \mid n p2n ,则 Φ ( n ) = Φ ( n p ) × p \varPhi(n) = \varPhi(\dfrac{n}{p}) \times p Φ(n)=Φ(pn)×p ;否则 Φ ( n ) = Φ ( n p ) × ( p − 1 ) \varPhi(n)= \varPhi(\dfrac{n}{p}) \times (p - 1) Φ(n)=Φ(pn)×(p1)

计算欧拉函数的模板代码如下,时间复杂度为 O ( n log ⁡ n ) O(n\log n) O(nlogn) ,全局变量 phi[] 中存储了 [ 1 , m a x n ] [1, maxn] [1,maxn] 中每个数的欧拉函数值:

const int maxn = 111111;
int minDiv[maxn], phi[maxn], sum[maxn];
void getPhi() {
	for (int i = 1; i < maxn; ++i) minDiv[i] = i;
	for (int i = 2; i * i < maxn; ++i) { // 埃式筛法计算每个数的最?质因子
		if (minDiv[i] == i)
			for (int j = i * i; j < maxn; j += i)
				minDiv[j] = i;
	}
	phi[1] = 1;
	for (int i = 2; i < maxn; ++i) {
		phi[i] = phi[i / minDiv[i]];
		if ((i / minDiv[i]) % minDiv[i] == 0) 
			phi[i] *= minDiv[i];
		else
			phi[i] *= minDiv[i] - 1;
	}
}

特别地,计算单个欧拉函数的值时,可以直接采用定义。


5. Mobius函数计算

计算 n n n 的Mobius函数 μ ( n ) \mu(n) μ(n) 。Mobius函数是做Mobius反演时的一个很重要的系数。Mobius函数的定义是:如果 i i i 的质因数分解式内,有任意一个大于 1 1 1 的指数,则 μ ( i ) = 0 \mu(i) = 0 μ(i)=0 ;否则, μ ( i ) \mu(i) μ(i) 等于 i i i 的质因数分解式内质数的个数   m o d     2 × ( − 2 ) + 1 \bmod\ 2 \times (-2) + 1 mod 2×(2)+1

Mobius函数有个很好的性质: ∑ d ∣ n μ ( d ) = [ n = 1 ] \displaystyle \sum_{d\mid n} \mu(d) = [n = 1] dnμ(d)=[n=1] ,其中 [ n = 1 ] [n=1] [n=1] 代表 n = 1 n=1 n=1 的时候为 1 1 1 n n n 不等于 1 1 1 的时候为 0 0 0 。由此可以递推地求出Mobius函数。

计算Mobius函数的模板代码如下,时间复杂度为 O ( n log ⁡ n ) O(n\log n) O(nlogn) ,全局变量 mu[] 中存储了 [ 1 , n ] [1, n] [1,n] 中每个数的Mobius函数值:

const int maxn = 1 << 20;
int mu[maxn];
int getMu() {
	for (int i = 1; i <= n; ++i) {
		int target = i == 1 ? 1 : 0;
		int delta = target - mu[i];
		mu[i] = delta;
		for (int j = i + i; j <= n; j += i)
			mu[j] += delta;
	}
}

计算单个Mobius函数时,可以直接采用定义。除了用埃式筛法外,还可用欧拉筛计算欧拉函数和Mobius函数。


6. 各大OJ经典题目

POJ 百炼3177,素数筛法
HDU 2138,素数判定
POJ 1811,大素数素性测试
HDU 1262,寻找素数对
HDU 2710,筛法求素数
洛谷 P3383 【模板】线性筛素数
HDU 3792,素数打表
POJ 1142,素因数分解
HDU 3826,分解质因子
HDU 6069,区间素数
POJ 2689,求 [ L , R ] [L, R] [L,R] 内的素数, 1 ≤ L < R ≤ 2147483647 ,   R − L ≤ 1 0 6 1 \le L < R\le 2147483647,\ R - L \le 10^6 1L<R2147483647, RL106
HDU 5901 Count Primes,求 1 ≤ n ≤ 1 0 11 1\le n \le 10^{11} 1n1011 范围内的素数个数
POJ 3090 欧拉函数计算

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

memcpy0

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

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

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

打赏作者

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

抵扣说明:

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

余额充值