质数判定
试除法,根据定义,枚举 [ 2 , n − 1 ] [2,n-1] [2,n−1] 中所有整数,看是否有能整除 n n n 的数 。
事实上,我们没有必要枚举出所有整数
a × b = n a\times b=n a×b=n,我们就说 a a a 和 b b b 是 n n n 的因数,所以因数都是成对的,并且对称分布在 n \sqrt n n 两边,我们只需要找各对因数中较小的一个,而较小的因数一定小于等于 n \sqrt n n
bool isPrime(int n)
{
if (n < 2) return false;
for (int i = 2; i <= n / i; ++i)
{
if (n % i == 0) return false;
}
return true;
}
注意:判断条件 i <= n / i
是最优的写法。不建议写成 i <= sqrt(n)
,因为 sqrt()
求根号的速度比较慢。也不建议写成 i * i <= n
因为 i * i
容易导致 int 溢出。
时间复杂度: O ( n ) O(\sqrt n) O(n)
质因数分解
每个合数都可以写成几个质数相乘的形式,其中每个质数都是这个合数的因数,把一个合数用质因数相乘的形式表示出来,叫做分解质因数。如 30 = 2 × 3 × 5 30=2\times3\times5 30=2×3×5, 10080 = 2 5 × 3 2 × 5 × 7 10080 = 2^5\times3^2\times5\times7 10080=25×32×5×7。分解质因数只针对合数。
求一个数分解质因数,要从最小的质数除起,一直除到结果为质数为止。分解质因数的算式叫短除法,和除法的性质相似,还可以用来求多个数的公因式。
短除法:
质因数分解代码:vector<pair<int, int>>
用于存放各质因数和对应的次数。
vector<pair<int, int>> divide(int x)
{
vector<pair<int, int>> res;
for (int i = 2; i <= x / i; ++i)
{
if (x % i == 0)
{
int s = 0;
while (x % i == 0)
{
x /= i;
++s;
}
res.push_back({ i, s });
}
}
if (x > 1) res.push_back({ x, 1 });
return res;
}
这样写之所以正确,是基于一个基本原理:一个数除 1 1 1 以外的最小的因数一定是质数。
反证法很容易证明这一点:假设一个数 x x x 除 1 1 1 以外的最小的因数 y y y 不是质数,那么 y y y 有除 1 1 1 和它本身的因数 z z z, z z z 一定也是 x x x 的因数,而 z < y z < y z<y,所以 y y y 不是 x x x 除1以外的最小的因数,与假设矛盾,原命题得证。
以上代码就是先找到这个数的第一个因数(以下所指的因数都不包括 1 1 1),它一定是质数,把它除干净之后得到一个新的数,新的数的最小的因数一定也是质数,而且比之前的大。最后如果 x x x 不能再分解,即 i i i 枚举到 x \sqrt x x 正好可以判断出 x x x 是个质数或者是 1,最后通过 x > 1 判断它也是一个质因数。
筛质数
找出 [ 1 , n ] [1,n] [1,n] 中所有的质数。
埃氏筛
将 [ 2 , n ] [2,n] [2,n] 全部列出来,依次划掉 2,3,4,5……n 的倍数。
对于任意一个 p 而言,如果它没有被划掉,那么说明它不是前面2~p-1的数的倍数,所以它一定是质数。
图解:
一开始可以确定 2 2 2 是质数,然后把 2 2 2 的倍数全部划掉; 3 3 3 没有被划掉,所以可以确定 3 3 3 是质数,然后把 3 3 3 的倍数全部划掉; 4 4 4 已经被 2 2 2 划掉了,它的倍数一定也是 2 2 2 的倍数,没必要再划了,跳过; 5 5 5 没有被划掉,可以确定 5 5 5 是质数,然后把 5 5 5 的倍数全部划掉……
像 4 4 4 这样的,合数的倍数不用再划,因为合数一定是前面某个数的倍数,那么它的倍数一定也是前面某个数的倍数。
这里还可以优化一个细节:比如我们在划 3 3 3 的倍数的时候,可以不从 3 × 2 = 6 3\times2=6 3×2=6 开始划,因为 6 6 6 已经被 2 2 2 划过了,而可以从 3 × 3 = 9 3\times3=9 3×3=9 开始划。同理,在划 5 5 5 的倍数的时候,应该从 5 × 5 = 25 5\times5=25 5×5=25 开始划,因为 5 × 2 5\times2 5×2、 5 × 3 5\times3 5×3、 5 × 4 5\times4 5×4 都被 2 2 2、 3 3 3 划过了。
代码:
开两个数组,vector<int> primes
用于存放质数,vector<bool> isPrime
用于记录这些数是否被划掉,初始化所有数为 true
,被划掉就标记成 false
。
void getPrimes(vector<int>& primes, vector<bool>& isPrime, int n)
{
for (int i = 2; i <= n; ++i)
{
if (isPrime[i])
{
primes.push_back(i);
for (long long j = (long long)i * i; j <= n; j += i)
{
isPrime[j] = false;
}
}
}
}
防止 i * i
导致 int
溢出的写法:
void getPrimes(vector<int>& primes, vector<bool>& isPrime, int n)
{
for (int i = 2; i <= n; ++i)
{
if (isPrime[i])
{
primes.push_back(i);
for (int j = i; j <= n / i; ++j)
{
isPrime[j * i] = false;
}
}
}
}
时间复杂度: O ( n log log n ) O(n\log\log n) O(nloglogn);这里的循环次数应该是 n 2 + n 3 + n 5 + ⋯ \frac{n}{2}+\frac{n}{3}+\frac{n}{5}+\cdots 2n+3n+5n+⋯,把 n n n 提出来就是 n ( 1 2 + 1 3 + 1 5 + ⋯ ) n(\frac12+\frac13+\frac15+\cdots) n(21+31+51+⋯),后面的一堆就是质数的倒数之和,它其实相当于 O ( log log n ) O(\log\log n) O(loglogn) ,所以总体的时间复杂度记为 O ( n log log n ) O(n\log\log n) O(nloglogn)
线性筛(欧拉筛)
为了提高效率,我们可以保证每个合数只被划掉一次,具体来说,是被它的最小质因数划掉。
我们知道,任何合数 x x x 都能分解质因数,因为合数的定义是不仅能被 1 1 1 和自己整除,还能被其它整数整除,这其它整数逐级分解最终还是质数。而且其质因数中,一定有小于等于 x \sqrt x x 的质因数,这是线性筛能筛掉所有合数的基本保证。
具体方法就是划掉所有 x 乘小于等于 x 的最小质因数的质数所得到的数 x = 2,3,4,5,6……,下面我们结合代码来看:
void getPrimes(vector<int>& primes, vector<bool>& isPrime, int n)
{
for (int i = 2; i <= n; ++i)
{
if (isPrime[i])
primes.push_back(i);
for (auto p : primes)
{
if (p * i > n) break;
isPrime[p * i] = false;
if (i % p == 0) break;
}
}
}
第 11 行,枚举到 i % p == 0
时 break
,因为我们是从小到大枚举当前得到的质数,所以
-
当
i % p == 0
时,p
一定是i
的最小质因数,同时p
一定是p * i
的最小质因数,划掉p * i
。 -
当
i % p != 0
时,p
比i
的最小质因数还要小,所以p
也一定是p * i
的最小质因数,划掉p * i
。
此两种状态都保证了,p * i
是被自己的最小质因数筛掉。
如果 i % p == 0
时不 break
,而是继续向后枚举质数,那么 p
就不是 p * i
的最小质因数了,因为此时 p > i的最小质因数
,i的最小质因数
也是 p * i
的质因数。而 i的最小质因数
不能保证是 p * i
的最小质因数,因为它不是从最小的质数开始枚举的。
对于一个合数
x
x
x,假设
p
p
p 是它的最小质因数,当 i
枚举到
x
/
p
x/p
x/p 的时候,
x
x
x 就被筛掉了,所以
x
x
x 永远比
i
i
i 快一步,如果
i
i
i 没被筛掉,可以确定
i
i
i 是质数。
时间复杂度: O ( n ) O(n) O(n)