信竞中的数学(一):质数
基本概念梳理
质数:对于所有大于 1 1 1 的自然数而言,如果该数除 1 1 1 和自身以外没有其它因数/约数,则该数被称为为质数,质数也叫素数。
合数:对于大于1的整数中,除了1和该数本身以外,还有其他因数,最小的合数是4,1既不是质数也不是合数。
约数:如果一个数 x x x 可以整除另外一个数 y y y,则 x x x 称为 y y y 的约数,约数也叫因数。
[ x , y ] [x, y] [x,y] : 闭区间,即包含两个端点 x x x 和 y y y,比如区间 [ 3 , 6 ] [3, 6] [3,6] 等价于 3 , 4 , 5 , 6 {3, 4, 5, 6} 3,4,5,6 四个数。
⌊ x ⌋ \lfloor x \rfloor ⌊x⌋ 表示 向下取整,即 ⌊ 5.7 ⌋ = 5 \lfloor 5.7 \rfloor = 5 ⌊5.7⌋=5 , ⌊ 3 ⌋ = 3 \lfloor 3 \rfloor = 3 ⌊3⌋=3 。
质数的应用
【例题1】试除法判定质数
给定 n ( 1 ≤ n ≤ 100 ) n(1\leq n \leq 100) n(1≤n≤100) 个正整数 a i ( 1 ≤ a i ≤ 2 31 − 1 ) a_i(1\leq a_i \leq 2^{31}-1) ai(1≤ai≤231−1),判定每个数是否是质数。
输入格式
第一行包含整数 n n n。
接下来 n n n 行,每行包含一个正整数 a i a_i ai。
输出格式
共 n n n 行,其中第 i i i 行输出第 i i i 个正整数 a i a_i ai 是否为质数,是则输出 Yes
,否则输出 No
。
输入样例:
2
2
6
输出样例:
Yes
No
【解析】试除法判定质数
如何判定一个数是否为质数呢?
最简单的办法就是试除法,即判断 x x x 是否为质数,可以枚举闭区间 [ 2 , x − 1 ] [2, x-1] [2,x−1] 中的所有整数,去尝试整除 x x x ;
如果区间中存在可以整除 x x x 的整数,则 x x x 不是质数;
如果闭区间中所有数都不能整除 x x x,则 x x x 为质数, 代码如下:
// isPrime: 判定 x 是否为质数,如果是,返回 true, 否则返回 false
bool isPrime(int x) {if(x < 2) return false; // 质数的定义中只考虑大于 1 的数for(int i = 2; i < x; i++) {if(x % i == 0) return false; // 在 [2, x-1] 中出现了约数,则可以断定不是质数}return true; // 遍历完所有的数都没有找到约数,则一定为质数
}
以上代码的时间复杂度为 O ( x ) O(x) O(x),是否可以优化呢?答案是可以的。
我们可以优化一下试除的区间,可以将区间缩小为 [ 2 , ⌊ x ⌋ ] [2, \lfloor \sqrt{x}\rfloor \ ] [2,⌊x⌋ ] ,思考为什么可以缩小到这个区间呢?
因为约数是成对出现的,比如 12 12 12, 约数有 2 , 3 , 4 , 6 2,3,4,6 2,3,4,6,其中 ( 2 , 6 ) (2, 6) (2,6) 是一对, ( 3 , 4 ) (3, 4) (3,4) 是一对;
如果该数 x x x 有约数,我们可以只枚举较小的约数,整除结果就是对应的较大的约数;
因此可以缩小枚举区间到 [ 2 , ⌊ x ⌋ ] [2, \lfloor \sqrt{x}\rfloor \ ] [2,⌊x⌋ ],如果在区间 [ 2 , ⌊ x ⌋ ] [2, \lfloor \sqrt{x}\rfloor \ ] [2,⌊x⌋ ] 中不存在 x x x 的约数,那在 [ ⌊ x ⌋ + 1 , x − 1 ] [\lfloor \sqrt{x} \rfloor + 1, x-1] [⌊x⌋+1,x−1] 区间中也不会存在 x x x 的约数。
但是在计算机中,求 x \sqrt{x} x 的效率更慢一些,所以我们可以等价替换为 i ∗ i ≤ x i*i \le x i∗i≤x,但是在极端情况下,可能会出现 i ∗ i i*i i∗i 越界导致出现错误,因此最推荐的写法为 i ≤ x / i i \le x/i i≤x/i, 代码如下:
// isPrime: 判定 x 是否为质数,如果是,返回 true, 否则返回 false
bool isPrime(int x) {if(x < 2) return false; // 质数的定义中只考虑大于 1 的数for(int i = 2; i <= x/i; i++) {if(x % i == 0) return false; // 在 [2, x-1] 中出现了约数,则可以断定不是质数}return true; // 遍历完所有的数都没有找到约数,则一定为质数
}
此时,时间复杂度优化为 O ( x ) O(\sqrt{x}) O(x),已达到最优。
【本题题解完整代码】
#include "bits/stdc++.h"
using namespace std;
int x, n;
// isPrime: 判定 x 是否为质数,如果是,返回 true, 否则返回 false
bool isPrime(int x) {if(x < 2) return false; // 质数的定义中只考虑大于 1 的数for(int i = 2; i <= x/i; i++) {if(x % i == 0) return false; // 在 [2, x-1] 中出现了约数,则可以断定不是质数}return true; // 遍历完所有的数都没有找到约数,则一定为质数
}
int main() {cin >> n;while(n--) {cin >> x;if(isPrime(x)) cout << "Yes\n";else cout << "No\n";}return 0;
}
【例题2】分解质因数
给定 n ( 1 ≤ n ≤ 100 ) n(1\leq n \leq 100) n(1≤n≤100) 个正整数 a i ( 2 ≤ a i ≤ 2 × 1 0 9 ) a_i(2\leq a_i \leq 2 \times 10^{9}) ai(2≤ai≤2×109),将每个数分解质因数,并按照质因数从小到大的顺序输出每个质因数的底数和指数。
输入格式
第一行包含整数 n n n。
接下来 n n n 行,每行包含一个正整数 a i a_i ai。
输出格式
对于每个正整数 a i a_i ai,按照从小到大的顺序输出其分解质因数后,每个质因数的底数和指数,每个底数和指数占一行。
每个正整数的质因数全部输出完毕后,输出一个空行。
输入样例:
2
6
8
输出样例:
2 1
3 12 3
【解析】分解质因数
任何一个大于 1 1 1 的数字都可以分解为若干个质数相乘,这个过程也叫分解质因数。
例如: 12 = 2 × 2 × 3 , 16 = 2 × 2 × 2 × 2 , . . . . . . 12 = 2 \times 2 \times 3, 16 = 2 \times 2 \times 2 \times 2, ...... 12=2×2×3,16=2×2×2×2,......
给定一个数 x x x,如果将该数分解成若干质因数相乘呢?通过试除法即可完成。
遍历闭区间 [ 2 , x ] [2, x] [2,x],如果当前遍历到的数字 i i i 可以整除 x x x,那么就用 i i i 整除 x, 直到不能整除即可停止,统计整除的次数 c n t cnt cnt 即为质因数 i i i 的指数。
代码如下:
// 将 x 分解质因数
void deal(int x) {for(int i=2; i<=x; i++) { // 试除区间int cnt = 0; // 当前质因数的个数while(x % i == 0) { // 如果 i 可以整除,那么一直整除统计个数cnt++;x /= i; // x 被整除}if(cnt > 0) cout << i << ' ' << cnt << endl; // 当前 i 是质因数}cout << endl;
}
上面的代码,有同学会有疑惑,不是说分解质因数嘛?为什么没有判断 i i i 为质数呢?
如果 x % i = = 0 x \% i == 0 x%i==0 成立的话, i i i 一定就是质数;
因为此时 x x x 在 [ 2 , i − 1 ] [2, i-1] [2,i−1] 中已经没有因数了(都被整除掉了),所以如果 x % i = = 0 x \% i == 0 x%i==0 成立的话,说明 i i i 在 [ 2 , i − 1 ] [2, i-1] [2,i−1] 区间中也没有因数了,即 i i i 为质数。
上面的代码时间复杂度为 O ( x ) O(x) O(x),能否优化呢?可以的。
我们知道, x x x 最多有一个大于 x \sqrt{x} x 的质因数,因此可以将遍历区间缩小到 [ 2 , x ] [2, \sqrt{x}] [2,x] 中,遍历结束后,如果 x x x 依然大于 1 1 1,那此时 x x x 一定就是那个质因数。
代码如下:
// 将 x 分解质因数
void deal(int x) {for(int i=2; i<=x/i; i++) { // 试除区间int cnt = 0; // 当前质因数的个数while(x % i == 0) { // 如果 i 可以整除,那么一直整除统计个数cnt++;x /= i; // x 被整除}if(cnt > 0) cout << i << ' ' << cnt << endl; // 当前 i 是质因数}if(x > 1) cout << x << ' ' << 1 << endl; // 最后一个最大的质因数cout << endl;
}
由于分解质因数过程中, x x x 一直被整除,因此假设最好的情况, x x x 刚好是 2 2 2 的次幂,那此时,代码时间复杂度为 O ( l o g x ) O(log\ x) O(log x)。
因此总的时间复杂度为 O ( l o g x ) O(log\ x) O(log x) 到 O ( x ) O(\sqrt{x}) O(x) 之间。
【本题题解完整代码】
#include "bits/stdc++.h"
using namespace std;
const int N = 1e6+7;
int n, x;
// 将 x 分解质因数
void deal(int x) {for(int i=2; i<=x/i; i++) { // 试除区间int cnt = 0; // 当前质因数的个数while(x % i == 0) { // 如果 i 可以整除,那么一直整除统计个数cnt++;x /= i; // x 被整除}if(cnt > 0) cout << i << ' ' << cnt << endl; // 当前 i 是质因数}if(x > 1) cout << x << ' ' << 1 << endl; // 最后一个最大的质因数cout << endl;
}
int main() {cin >> n;while(n--) {cin >> x;deal(x);}return 0;
}
【例题3】线性筛质数
题目描述
如题,给定一个范围 n n n,有 q q q 个询问,每次输出第 k k k 小的素数。
输入格式
第一行包含两个正整数 n , q n,q n,q,分别表示查询的范围和查询的个数。
接下来 q q q 行每行一个正整数 k k k,表示查询第 k k k 小的素数。
输出格式
输出 q q q 行,每行一个正整数表示答案。
输入
100 5
1
2
3
4
5
输出
2
3
5
7
11
说明/提示
【数据范围】
对于 100 % 100\% 100% 的数据, n = 1 0 8 n = 10^8 n=108, 1 ≤ q ≤ 1 0 6 1 \le q \le 10^6 1≤q≤106,保证查询的素数不大于 n n n。
【解析】线性筛质数
根据上面分解质因数可以知道,任何一个合数均可以分解为若干个质因数相乘,那是否可以将所有的合数标记出来, 2 ~ n 2~n 2~n 中,所有已标记的合数的补集就是所有质数。
为什么要这样做呢?因为如果直接找质数,一定需要对每个数进行 c h e c k check check 判断,但是对于合数来说,不需要判断,我们可以通过乘积的方式进行快速标记。
如何记录第 k k k 个素数是谁呢?在标记的时候,由于标记是往后进行的,所以遍历到 i i i 的时候,如果 i i i 没有被标记,那么一定就是质数, c n t cnt cnt 从 1 1 1 开始,遇到一个质数,保存起来,并 c n t + + cnt++ cnt++,表示寻找下一个质数。
【算法步骤】
1 1 1、 定义 i s N o t P r i m e [ i ] isNotPrime[i] isNotPrime[i] 为 t r u e true true 表示 i i i 不是质数,即 i i i 是合数;
定义 p r i m e [ i ] prime[i] prime[i] 表示第 i i i 个质数;比如 p r i m e [ 1 ] = 2 , p r i m e [ 2 ] = 3 , p r i m e [ 3 ] = 5 , . . . prime[1]=2,prime[2]=3, prime[3]=5,... prime[1]=2,prime[2]=3,prime[3]=5,... 表示第一个质数是 2 2 2,第二个质数是 3 3 3,第三个质数是 5 , . . . 5,... 5,... 等等
2 2 2、遍历 i i i , i i i 属于 [ 2 , n ] [2,n] [2,n] 闭区间, 将 i i i 之前的所有质数都保存在 p r i m e prime prime 数组中,可以用 i i i 乘以所有保存的质数得到 y y y,那么 y y y 一定不是质数,所以可以标记 i s N o t P r i m e [ y ] isNotPrime[y] isNotPrime[y] 为 t r u e true true,遍历到 i i i 以后,如果 i s N o t P r i m e [ i ] isNotPrime[i] isNotPrime[i] 依然为 f a l s e false false,没有被标记,那么可以说 i i i 就一定是质数,需要保存起来。
3 3 3、对于有些合数比如 12 12 12,既可以分解为 2 × 6 2\times 6 2×6,也可以分解为 3 × 4 3\times 4 3×4,如果不加入限制,则很多数会被大量重复标记,导致效率低下,时间复杂度上升。
如何限制呢?对于 i i i 而言,如果 i % p r i m e [ j ] = = 0 i\%prime[j]==0 i%prime[j]==0,则说明一定存在一个数 x x x,使得 p r i m e [ j ] × x = = i prime[j]\times x==i prime[j]×x==i(公式 1 1 1);
如果此时继续往后走,将会标记 p r i m e [ j + 1 ] × i prime[j+1]\times i prime[j+1]×i 为非质数,用公式 1 1 1 替换,可以得到下一个要标记 p r i m e [ j + 1 ] × p r i m e [ j ] × x prime[j+1]\times prime[j]\times x prime[j+1]×prime[j]×x;
由于 p r i m e [ j ] < p r i m e [ j + 1 ] prime[j] < prime[j+1] prime[j]<prime[j+1] ( $ prime[j]$ 是第 j j j 个质数, p r i m e [ j + 1 ] prime[j+1] prime[j+1] 是第 j + 1 j+1 j+1 个质数),
因此下一个要标记的数 p r i m e [ j + 1 ] × p r i m e [ j ] × x prime[j+1]\times prime[j]\times x prime[j+1]×prime[j]×x 可以在 i = = p r i m e [ j + 1 ] × x i==prime[j+1]\times x i==prime[j+1]×x 的时候用更小的质数 p r i m e [ j ] prime[j] prime[j] 进行标记,
因此就不需要大的质数 p r i m e [ j + 1 ] prime[j+1] prime[j+1] 乘以小的 i = = p r i m e [ j ] × x i==prime[j] \times x i==prime[j]×x 来标记了;
举例说明:比如当 i i i 循环到 4 4 4 的时候,此时保存的质数有 2 2 2 和 3 3 3 ,那么 2 × 4 = 8 2\times 4=8 2×4=8,所以标记 8 8 8,由于 4 % 2 = = 0 4\%2==0 4%2==0 ,所以就可以停止了,不用再往后标记了,再往后就是 3 × 4 3\times 4 3×4 标记 12 12 12 了,由于 i i i 循环到 6 6 6 的时候,可以用 2 × 6 2\times 6 2×6 标记 12 12 12,因为质数 2 2 2 小于 质数 3 3 3,所以对于同一个数,我们用最小质因数标记,可以防止重复标记,增加效率。
【本题题解完整代码】
#include "iostream"
using namespace std;
int n , q , k , x;
const int N = 1e8+7;
bool isNotPrime[N]; // 如果isNotPrime[i]为true,则i不是素数,否则i是素数
int prime[N]; // prime[i]表示第i个素数是多少
int cnt = 1; // cnt表示当前将要存储第cnt个质数,cnt从1开始
int main()
{cin >> n >> q;for (int i = 2;i <= n;i++) { // i从2遍历到nif(!isNotPrime[i]) prime[cnt++] = i; // 如果i没有被标记过,说明i一定是质数,保存到prime数组中,cnt++,要保存下一个质数了for(int j=1; j<cnt && prime[j] * i <= n; j++) { // 找出目前已经保存的所有质数,与i做乘积,乘积后标记为非质数isNotPrime[i*prime[j]] = true; // 标记非质数if(i%prime[j] == 0) break; // 遇到这种情况就break,因为后面的非质数,可以用更小的质数标记,只需要i大一点就行了,}}while(q--) {cin >> x; // 询问cout << prime[x] << endl; // 直接输出即可}return 0;
}