质数的概念与判定
若一个正整数$ N $为质数,那么不存在一个数$ M(2\le M\le \sqrt{N}) $,使得$ M $整除$ N $. 一个足够大的数$ N $之前大约有$ N / \ln{N} $个质数。
由此我们得出了判断一个数是不是质数的朴素方法:
1 | bool isPrime(int n) { |
但是这个方法效率很低,仅仅判定一个数就达到了$ O(\sqrt{N}) $的时间复杂度,需要优化。
Miller_Robbin方法
先来回顾一下费马小定理:
如果$ p $是一个质数,$ a $是一个整数,那么有$ a^p \equiv a;(mod; p) $,如果$ a $不是$ p $的倍数,那么$ a^{p-1} \equiv 1;(mod; p) $.
依据费马小定理,我们只需遍历小于$ p $的数,并判断上述第二个式子是否成立即可,但是这样复杂度甚至比刚才的朴素判断方法还要低。事实上,可以证明的是,对于一个小于$ p $的数,我们每进行一次判断,我们至少排除了p不是素数的一半的可能性。于是,我们只需选取一个合适的数量,比如判断100次,那么此时p不是素数的概率只有$ (1/2)^{100} $,这个概率已经足够说明p就是一个素数了。在计算次方的时候,使用快速幂,记得随时取模。
1 | int modpow(long long x, int n, int mod) { |
该算法的时间复杂度为$ O(C\log^{2}{N}) $,其中$ C $为选取的判断次数。
Eratosthenes筛法
对于少量质数的判断,使用上面的方法比较好,但是如果一次需要使用大量的素数,使用上面的方法,就显得效率非常低了。考虑这样一个问题:求出$ 1-N $之间的所有素数。对于这个问题,我们不可能一个个数去检查。Eratosthenes筛法的思想很简单,就是一个数的倍数一定不是质数。所以从2开始扫描每个数,标记它的所有倍数为不是素数即可。
1 | void getprimes(int n) { |
对于Eratosthenes筛法,有两个简单的优化,一个是对于一个数,只需要用素数去筛去即可,另外一个是,当前数的平方之前的数,一定已经被筛选过了。优化后的代码如下:
1 | void getPrimes(int n) { |
代码的时间复杂度为$ O(N\log{\log{N}}) $.
Euler筛法
Eratosthenes筛法的复杂度已经非常接近线性了(但是我们依然不满足),Euler筛法基于这样一个原则,每一个数只会被它的最小质因数筛掉。先看代码,具体后面再解释。
1 | void getPrimes(int n) { |
别的部分好理解,但是为什么$ primes[j] $整除$ i $的时候需要跳出循环呢?我们假设$ i = k * primes[j] $,那么$ i * primes[j+1] = k * primes[j] * primes[j+1] $,也就是说,$ i * primes[j+1] $一定有一个小于$ primes[j+1] $的质数(也就是$ primes[j] $)已经或者会在之后将其筛除,这样就保证了一个数不会被多个不同的质因子重复筛选多次。
算法的时间复杂度为$ O(N) $.
Euler筛法的另一种代码实现如下,其中$ v $记录着一个数的最小质因子:
1 | void getPrimes(int n) { |
质因数分解
算术基本定理:任何一个大于1的正整数可以唯一分解为有限个质数的乘积,也就是$ N = p_1^{c_1}\times p_2^{c_2}\times p_3^{c_3}\times\cdots \times p_n^{c_n} $.
我们使用最朴素的试除法来分解质因数,每次找到一个质因数,那么就不断除以这个质因数直到除尽为止。代码中的$ c $数组存放的是各个质因数的次数(对应于$ primes $数组)。
1 | void factorize(int n) { |
算法的时间复杂度为$ O(\sqrt{N}) $.