质数章-数论代码笔记(超级细节)

 1. 判断是否质数

试除法,注意边界条件,不能写成 i * i <= x,i大时会越界,也不要写i < sqrt(x),

这样每次循环都会调用sqrt,比较慢。

bool is_prime(int x)
{
    if (x < 2) return false;
    for (int i = 2; i <= x / i; i ++ )
        if (x % i == 0)
            return false;
    return true;
}

2. 试除法分解质因数

试除法。 注意边界条件(与上面一样)。

关键是思想,从小到大用质数去除。注意这里为什么不用判断i是不是质数,因为如果x % i == 0,假设这里的i不是质数,那么i一定能表示成 i = a * b的形式,这里的a或b一定有一个小于i的,也就是说,a或b一定是x的因子。但是由于我们是从小到大遍历i,并且除去的。所以遍历到i时,x一定没有小于i的质数,与前面矛盾。所以当x % i == 0时,i一定是质数。

注意末尾(注:这是因为如果x有一个大于 x / 2的因子,那么一定只有一个)判断下 x  > 1(这里写成>1是为了方便,事实上,为了容易理解,我们可以在一开始保存n = x, 经过for循环之后判断 x >= n / 2 就可以。直接写x > 1是为了少写一个变量。

void divide(int x)
{
    for (int i = 2; i <= x / i; i ++ )
        if (x % i == 0)
        {
            int s = 0;
            while (x % i == 0) x /= i, s ++ ;
            cout << i << ' ' << s << endl;
        }
    if (x > 1) cout << x << ' ' << 1 << endl;
    cout << endl;
}

3. 朴素筛求质数

刚刚我们的算法1是判断一个质数是不是质数,那如果我们想判断一个区间中有多少质数怎么办呢,显然我们可以直接使用n次算法1。但是我们有更好的做法。

用我们刚刚算法二中的思想,我们从小到大遍历i,每次遍历到一个质数,就把范围内所有是这个质数倍数的数都记录为false。当我们遍历到false时直接跳过,遍历到true时再去标记。

这样做的正确性也很容易证明。因为我们任何一个合数都可以分解为质因数乘积的形式,那么我们遍历到一个质数的时候,会把对应的倍数给标记为false。由于我们是从小到大遍历,当遍历到一个数时,如果它没被标记,说明它一定不能被小于它的质数分解,那么它也一定是质数。

可以与算法二简单记忆一下,因为这两个算法都是遍历到一个数为true时,这个数一定是质数。

代码为:

int primes[N], cnt;     // primes[]存储所有素数
bool st[N];         // st[x]存储x是否被筛掉

void get_primes(int n)
{
    for (int i = 2; i <= n; i ++ )
    {
        if (st[i]) continue;
        primes[cnt ++ ] = i;
        for (int j = i + i; j <= n; j += i)
            st[j] = true;
    }
}

特别注意一点,第二个for循环写成j+=i的形式,不要真的把倍数写成乘法的形式比如 k = j * m(m从2开始递增),加法是比乘法快一点的。

4. 线性筛求质数

容易发现算法三筛的还有改进的空间,因为同一个数,可能会被标记多次,比如6会被2标记,也会被3标记,怎么改进呢?我们可以使用线性筛来解决这个问题。

根据唯一分解定理可知,每个合数都有一个最小素因子。而欧拉筛的基本思想是,让每个合数被其自身的最小素因子筛选,而不会被重复筛选。欧拉筛的框架和埃氏筛大致相同,区别点在于第二层循环对倍增过程的操作。

朴素筛是,只要是素数就进行倍增。而欧拉筛是用当前遍历到的数字i,去乘以已经在素数表中的素数。

我们先放代码,再对代码进行讲解。

int primes[N], cnt;     // primes[]存储所有素数
bool st[N];         // st[x]存储x是否被筛掉

void get_primes(int n)
{
    for (int i = 2; i <= n; i ++ )
    {
        if (!st[i]) primes[cnt ++ ] = i;
        for (int j = 0; primes[j] <= n / i; j ++ )
        {
            st[primes[j] * i] = true;
            if (i % primes[j] == 0) break;
        }
    }
}

因为 i 是从小到大进行循环,会乘以前面的每一个素数,这就保证了每个素数的倍数都不会被错过。也就是每个合数都会被筛掉,这就保证了算法的正确性。

注意奇怪的是有一个语句,我们为什么要加上这一句话呢?

if (i % primes[j] == 0) break;

为什么会有这句话呢?

当前数能被质数数组中的数整除,当前数一定是包含这个质数因子的合数。此时我们直接跳出,就意味着我们认为后面的合数一定能够之后被遍历到。

此时我们有i * primes[j+1],i * primes[j+2]..... 没有遍历到,因为i % primes[j] == 0, 所以i可以表示成i = a * primes[j],那么i * primes[j+1] 可以表示成a * primes[j] * primes[j+1],那么当i遍历到a* primes[j+1]时,一定能用primes[j]去筛掉i * primes[j+1],同理可得当i遍历到a*primes[j+2]时,一定能用primes[j]去筛掉i * primes[j+2]。而且此时的primes[j]一定是最小的质数。

阅读完毕,请去快速训练一些题目吧~

判断质数
204. 计数质数

介绍完毕质数~下次我会分享约数的相关算法。

  • 8
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值