算法中的数学:质数(素数)
1.质数
1.1定义
一个大于1的自然数,除了1和它自身外,不能被其他自然数整除,那么他就是质数,否则他就是合数。
注意:1既不是质数也不是合数
唯一的偶质数是2,其余所有质数都是奇质数
1.2质数判定求法
试除法:
若n为1,说明不是质数,
若n不为1,
我们从2开始遍历到n-1,判断2到n-1是否可以被n整除,如果任意(2,n-1)的数可以被整除,那么他就不是质数。如果都不能被整除,那么就是质数
上面的试除法其实还不够好,因为需要遍历的次数接近n次,而我们其实可以再剪掉一部分数不用判断。
结论:只要遍历从2到根号n的自然数
讲解:
若n有一个大于根号n的因数,那么它一定有一个小于根号n的因数(因为这样两个因数相乘才会等于n)
而一个合数一定有一个因数小于等于根号n,所以我们只要判断从2到根号n的自然数是否有关于n的因数就可以知道该数是合数还是质数了
若存在一个可以整除的数i,那么这个数n就是合数,否则他就不是合数,而是质数
代码实现:
bool isprime(int num) {if(num == 1) return false;for(int i = 2; i <= num/i; i++){if(num%i == 0){return false;}}return true; }
2.更多应用方法
若我们想知道区间(1,n)中一共有多少个素数,且想知道第k个素数是谁。应该如何求?
暴力解法:遍历判断(1,n),用全局变量count记录一共出现的素数个数,用数组f[k]记录第k个素数是谁
不过我们其实有其他更好的解法可以用,接下来我们分别介绍
注意:区间一定要是从1或2开始的连续区间才可以用后面的方法
2.1埃氏筛
对于一个大于1的正整数,他的k倍一定是一个合数
所以我们在访问到当前数num的时候就可以顺便把他的所有倍数剪枝掉,他的所有倍数一定不是素数
而如果我们是从i==2开始遍历的,只要我们可以访问到的数就一定是质数,这可以用反证法推导。
图示解析前三轮:
被框柱的就是判定为素数的,被划掉的表示经过倍数筛查将这些合数筛掉了,我们看到这里其实有一些数被筛掉了两次,这是因为他们是某些质数的公倍数,而我们其实可以通过优化让我们在筛查的时候减少重复筛
优化:
我们在倍数筛查的时候可以直接从i的i倍开始,因为i的2倍到i-1倍都被前面的质数倍数筛掉了
代码实现:
typedef long long ll; const int N = 1e9; bool f[N]; int a[N]; int count; void get_prime(int n) {for(int i = 2; i <= n; i++){if(f[i] == true){ continue;}a[++count] = i;for(ll j = i*i ; j <= n; j+=i){f[j] = true;}} }
2.2线性筛
由于埃氏筛即使经过从i乘i开始的优化,仍然会有合数被重复筛,时间复杂度并不是O(n)级别。
为了使筛选的时间复杂度降低为O(n)级别,我们需要使用线性筛
线性筛最多只会对每个元素访问两次,一次是遍历到的时候,一次是筛掉的时候。
核心逻辑:让每个合数被其最小质因数筛掉
代码实现核心逻辑:
从前往后遍历数i,用i的质数倍去筛,当i是当前的质数的倍数时,进行完当前筛查后停止
代码实现:
typedef long long ll; const int N = 1e9; bool f[N]; int a[N]; int count; void get_prime(int n) {for(ll i = 2; i <= n; i++){ //没有被标记为true,说明是质数if(f[i] == false) {a[++count] = i;} //进行线性筛for(int j = 1; i*a[j] <= n; j++){f[i*a[j]] = true;if(i % a[j] == 0) break;{ } }
注意:
1.如果i是合数,那么他会枚举到最小质因数后退出循环;如果i是质数,那么他会枚举到等于它本身的质数后退出循环。
而这个特性表明:p[j]就是i的最小质因数
2.线性筛的for循环条件中没有j<=count是因为当j==count的时候,i==p[j],直接break了
2.3例题讲解
审题:
本题需要我们找出区间l到r之间的所有素数的个数思路:
方法一:暴力解法
我们用双层for循环,外层for循环将l到r遍历,内层将1~根i的数与外层的数依次试除,然后若有一次成功整除则i为素数,count++。
时间上:外层为1e6,内层为1e6,所以总共的运行次数可能会达到1e12,会超时
方法二:二次筛选
我们其实不用将1到r的所有素数都选出来,而是只要遍历到根号r即可将1到r的素数筛选出来。
然后我们的筛选方法不用试除法,因为该方法要对每一个数都进行n次,所以使用埃氏筛。
第一步:将1到根号r的质数筛选出来
第二步:利用筛选出来的质数再利用埃氏筛的方法筛选出l到r之间的素数
第三步:记录区间素数个数并输出
注意:
1.筛选1到根号r的素数的目的是利用埃氏筛进行l到r的筛选2.如何确定质数的起始倍数?
因为我们的l不是从1或2开始的,所以质数的较低倍数可能会小于l,我们为了避免这种无效筛选就要找到质数合适的起始倍数。而该质数乘上起始倍数一定要大于等于l的值
于是我们得出以下向上取整的倍数计算式: l+x-1/x
解析:原本l/x就可以大概得到所需倍数,但是可能因为被计算机向下取整导致计算结果仍小于l,所以我们在分子加上x-1就进行了向上取整的操作
解题:
#include<iostream> #include<cmath> using namespace std; typedef long long ll; const int N = 1e6 + 10; ll l, r; int cou;//记录1~根号r的素数个数 ll prime[N];//记录素数 bool f[N];//埃氏筛筛查表 bool F[N]; void selprime() {for (ll i = 2; i <= sqrt(r); i++){if (F[i] == true)//被筛掉了{continue;}else{prime[cou++] = i;ll j = i;while (j * i <= sqrt(r))//进行标记{F[j * i] = true;j++;}}} } int main() {cin >> l >> r;l = l == 1 ? 2 : l;//控制l不为1//筛选1~根号r的素数selprime();//利用埃氏筛和选出的素数对l到r区域进行筛选for (int i = 0; i < cou; i++){ll x = prime[i];for (ll j = max(2 * x, (l + x - 1) / x * x); j <= r; j += x){f[j-l] = true;}}//记录区间素数个数ll count = 0;for (ll i = l; i <= r; i++){if (f[i-l] == false)count++;}cout << count << endl;return 0; }
1.我们的第一次筛选使用的是埃氏筛,所以利用了一个F数组来记录筛选结果。
2.由于1既不是合数也不是素数,所以我们将l为1的情况变为l为2
3.若出现l的值和筛选出的质数的值一样的情况,也就是起始倍数为1,我们不能对它打上true标记,因为他是质数,所以我们加一个判断,若倍数为1则将倍数设置为2进行筛选
4.由于l和r的数据范围较大,为了合法记录筛选情况,我们不能直接用f[j]记录情况,而是要将j情况映射到j-l处,因为l和r之间的差小于1e6,我们是可以记录的