算法中的数学:质数(素数)

发布于:2025-05-18 ⋅ 阅读:(13) ⋅ 点赞:(0)

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,我们是可以记录的


网站公告

今日签到

点亮在社区的每一天
去签到