质数

试除法判质数

算法思想

由于算法比较简单,就不再从朴素一步步进行优化了,直接写最终版本
一个数n的约数都是成对存在的,且一个位于 \(\sqrt[2]{n}\) 前面,一个位于后面。所以只需要判断从2到\(\sqrt[2]{n}\)的数是不是约数即可

代码实现

/**
 * 线性筛(欧拉筛)核心:一个数只会被它的最小质因子筛掉
 */
 
#include <iostream>
#include <cmath>

using namespace std;

/**
 * 写法1
 * 缺点每次都需要调用sqrt,耗费时间
 */
bool is_prime(int x)
{
    if (x < 2) return false;
    for (int i = 2; i <= sqrt(x); ++ i)
        if (x % i == 0) return false;
    return true;
}
/**
 * 写法2
 * 会被这组数据卡掉
 * 1
 * 2147483647
 * 46340*46340<2147483647,但46341*46341就爆int了
 * 缺点:当x较大时,(i-1)*(i-1)还小于x,但是i*i可能就爆int了
 */
bool is_prime(int x)
{
    if (x < 2) return false;
    for (int i = 2; i * i <= x; ++ i)
        if (x % i == 0) return false;
    return true;
}
/**
 * 写法3
 * 对于i <= x / i有两种理解方式
 * 1.从 (i <= sqrt(x)) -> (i * i <= x) -> (i <= x / i),就是公式的变形,没什么好说的
 * 2.i表示根号n左边的约数,n/i表示对应i的根号n右边的约数,保证左边的约数<=右边的约数即可
 * 优点:解决的写法1调用函数的耗时问题和写法2超出数据范围的问题
 */
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;
}
int main()
{
    int n;
    cin >> n;
    while (n --)
    {
        int a;
        cin >> a;
        if (is_prime(a)) cout << "Yes" << endl;
        else cout << "No" << endl;
    }
    
    return 0;
}

试除法分解质因数

方法1:遍历范围内所有数

算法原理
首先说明几个性质

  1. n的质因子中最多只有一个大于 \(\sqrt[2]{n}\)

证明:假设有两个大于 \(\sqrt[2]{n}\) 的因子,那么两者的乘积自然 > n,显然是矛盾的

  1. 如果a | b,并且b中没有 [2, a - 1] 中的质因子,那么a中也一定没有 [2, a - 1] 中的质因子

证明:根据唯一分解定理,a和b均为表示为质数的乘积,a如果有b没有的质因子,那么a一定无法整除b 或者根据a|b,可以等价为b=ka,所以a的质因子只会比b少不会比b多

首先根据性质1,我们寻找质因数一定是在2到\(\sqrt[2]{n}\)中进行寻找,
然后对于这个范围内每一个数,我们都判断一下是不是因子,如果是一直除直到无法除为止
所以我们需要证明的是我们选择到的i(x % i == 0)一定是质数

证明:此时遍历到i(x % i == 0),说明x中一定没有了2到i-1中的质因子,根据定理2,i也没有2到i-1中的质因子,根据定理1,i的唯一质因子只能是它本身,所以说明i就是质数,
所以说明虽然我们遍历的数据中包含合数,但是选择到的数一定都是质数,所以可以完成质因数分解

代码实现
时间复杂度为\(O(log_n) \sim O(\sqrt{n})\)。因为当\(n = 2^k\)时,仅需要\(log_2n\)次运算。
而且,从代码的实际表现来看,很多情况下复杂度是小于\(O(\sqrt{n})\)的。例如在Hankson的趣味题一题中,使用该方法理论复杂度和试除法求约数的复杂度都是\(O(\sqrt{n})\),但是试除法却超时了。
因为内外层循环之间存在关联,故对于该算法\(O(\sqrt{n})\)的复杂度仍存在疑惑。

#include <iostream>

using namespace std;

int n;

void divide(int x)
{
    for (int i = 2; i <= x / i; ++ i)  // 为什么这随着x的变化也是对的,假设经历了一次循环,x变成了x1,i也变成了i1,我们完全可以看为此时就是对x1进行质因数分解,我们还能够保证x1不包含i这个质因子,所以完全可以从i1开始,仍然循环到sqrt(x1)
        if (x % i == 0)
        {
            int cnt = 0;
            while (x % i == 0)
            {
                x /= i;
                ++ cnt;
            }
            cout << i << ' ' << cnt << endl;
        }
    if (x > 1) cout << x << ' ' << 1 << endl; // 处理唯一可能大于根号x的那个质因子
    cout << endl;
}
int main()
{
    cin >> n;
    while (n --)
    {
        int x;
        cin >> x;
        divide(x);
    }
    
    return 0;
}

方法2:仅遍历质因子

算法原理
首先说明以下几个性质

  • 性质1: \(n\)的质因子中最多只有一个大于 \(\sqrt{n}\)
  • 性质2: \(1\)\(n\)内的素数个数近似为 \(\frac{n}{ln(n)}\)
    根据性质1,可以得出结论“我们在对n进行质因数分解时,只需要找到\(\sqrt{n}\)以内的素数即可,根据性质2,\(\sqrt{n}\)以内的素数有\(\frac{\sqrt{n}}{ln(\sqrt{n})}\)
    我们只需要将试除法中试除的从\(2\)\(\sqrt{n}\)的所有数变为\(\sqrt{n}\)以内的素数即可,所以时间复杂度由\(O(\sqrt{n})\)降到了\(O(\frac{\sqrt{n}}{ln(\sqrt{n})})\)
    考虑时间复杂度应该考虑算法的主体部分,虽然方法2在试除法之前需要先进行一次\(O(n)\)的线性筛,但这并不属于算法的主体,所以该复杂度并没有考虑进试除法的复杂度内。
    从复杂度上看,方法2应该是略快与方法1的,但是实际表现是不但没有变快,反而变慢了,线性筛的复杂度是不是需要考虑
    进去?

代码实现

#include <iostream>

using namespace std;

const int N = 50010;

int primes[N], cnt;
bool st[N];

void init(int n)
{
    for (int i = 2; i <= n; ++ i)
    {
        if (!st[i]) primes[cnt ++] = i;
        for (int j = 0; primes[j] * i <= n; ++ j)
        {
            st[primes[j] * i] = true;
            if (i % primes[j] == 0) break;
        }
    }
}
void divide(int x)
{
    for (int i = 0; i < cnt; ++ i)
    {
        int p = primes[i];
        if (x % p == 0)
        {
            int s = 0;
            while (x % p == 0)
            {
                x /= p;
                ++ s;
            }
            cout << p << ' ' << s << endl;
        }
    }
    if (x > 1) cout << x << ' ' << 1 << endl; // 处理唯一可能大于根号x的那个质因子
    cout << endl;
}
int main()
{
    init(50000);
    
    int n;
    cin >> n;
    while (n --)
    {
        int x;
        cin >> x;
        divide(x);
    }
    
    return 0;
}

质数筛法

1到n内的素数个数近似为 \(\frac{n}{ln(n)}\)

朴素版

算法思想

O(nlogn)
算法原理:把每一个数的倍数排除掉
正确性证明:如果某个数p没有被排除掉,说明2到p-1中没有p的约数,根据质数的定义,p是一个质数。即算法保证了最后剩下的数一定是质数

代码实现

#include <iostream>

using namespace std;

const int N = 1e6 + 10;

int n;
bool st[N];
int primes[N], cnt;

void get_primes(int x)
{
    for (int i = 2; i <= x; ++ i)
    {
        if (!st[i]) 
            primes[cnt ++] = i; // 如果是素数就保存
        for (int j = i + i; j <= n; j += i) // 加法比乘法更快
            st[j] = true;
    }
}
int main()
{
    cin >> n;
    get_primes(n);
    cout << cnt << endl;
    return 0;
}

埃式筛

算法思想

O(nloglogn)
对于12这个数,朴素做法中2,3,4,6都会去筛它
根据唯一分解定理12=2^2*3,所以我们只需要使用质数2和3去筛掉它即可
算法原理:把每一个质数的倍数排除掉,这里需要反过来想,因为根据唯一分解定理,任何一个数都可以表示为质数的乘积,所以用2到p-1的质数就一定可以筛掉合数p,留下的就是质数p

代码实现

#include <iostream>

using namespace std;

const int N =1e6 + 10;

int n;
bool st[N];
int primes[N], cnt;

int get_primes(int x)
{
    for (int i = 2; i <= x; ++ i)
    {
        if (st[i]) continue; // 合数跳过
        primes[cnt ++] = i;
        for (int j = i + i; j <= n; j += i)
            st[j] = true;
    }
}
int main()
{
    cin >> n;
    get_primes(n);
    cout << cnt << endl;
    return 0;
}

线性筛(欧拉筛)

算法思想

线性筛(欧拉筛) O(n)
算法思想:每一个只用它的最小质因子筛掉
埃式筛使用质数筛掉所有合数虽然减少了筛的次数,但是像12还是会被2和3重复筛一次
线性筛就是保证12只会被2筛掉,即对于任何一个合数仅使用它的最小质因子去筛掉它,因为每个数只会被筛一次,所以称之为线性筛

  1. 如何保证只用最小的质因子筛掉合数n?
  1. i % primes[j] == 0时,primes[j]一定是i的最小质因子,因为我们是从小到大遍历质数的,第一个找到的就是最小的,所以primes[j]也就是primes[j] * i的最小质因子,而primes[j] * i就是我们要筛掉的数
  2. i % primes[j] != 0时,说明primes[j]是小于i的最小质因子的(注意这里是有前提的,必须是在i % primes[j] == 0之前才能说明primes[j]是小于i的最小质因子的),所以primes[j]就是primes[j] * i的最小质因子
    上面两条保证了无论是否退出循环,primes[j]一定是被筛掉的数的最小质因数
  1. 为什么i % primes[j] == 0后就不再用后面的质数去筛了?

为了满足primes[j]一定是i的最小质因子就必须满足上面的两条,第2条中有一个条件是 在i % primes[j] == 0之前才能说明i % primes[j] != 0的primes[j]是小于i的最小质因子
在i % primes[j] == 0之后,再出现的i % primes[j] != 0的primes[j]就不再保证是要被筛掉的数的最小质因子了

  1. 如何保证所有合数都被筛掉

对于合数x,如果p是其最小质因子,当i枚举到x/p时,x就一定被筛掉了。也就是对于任意一个合数,在i枚举到它之前就已经能够把它筛掉了,到x时如果没筛掉说明它的最小质因子就是他自己,即它就是质数

代码实现

#include <iostream>

using namespace std;

const int N = 1e6 + 10;

int n;
bool st[N];
int primes[N], cnt;

void get_primes(int x)
{
    for (int i = 2; i <= x; ++ i)
    {
        if (!st[i]) primes[cnt ++] = i;
        /**
         * 此处可以不加j<cnt的条件
         * 当i为质数时,primes[cnt-1]=i,i % primes[j] == 0最后一定会成立,循环肯定会停止,所以j<cnt不加是可以的
         * 当i为合数时,最多只有一个质因子大于sqrt(i),所以2到i-1中一定有i的质因子,所以循环一定会停止
         * 
         * 那既然通过if (i % primes[j] == 0) break;都可以结束循环,为什么还需要 primes[j] <= x / i;的限制?
         * 这个是保证我们要筛掉的数据在规定范围内,超出范围的数就不再需要判断了
         */
        for (int j = 0; primes[j] <= x / i; ++ j)
        {
            st[i * primes[j]] = true;
            if (i % primes[j] == 0) break;
        }
    }
}
int main()
{
    cin >> n;
    get_primes(n);
    cout << cnt << endl;
    return 0;
}

质数二次筛法

解决问题类型
求解某一区间内的所有质数,该区间满足两点要求:

  1. 端点数值很大: [L, R], \(1 ≤ L < U ≤ 2^{31}−1\)
  2. 区间距离较小: [L, R], \(L\)\(U\) 的差值不会超过 \(10^6\)

分析思路
首先考虑采用线性筛法,线性筛法的一个很重要的特点是只能从1开始筛,无法直接选取某个区间。所以由于区间端点数值较大,直接采用线性筛法无论在时间还是在空间上都不能满足要求。
但是我们注意到虽然区间端点数值较大,但是区间距离较小,所以对于某个区间,如果我们能够使用一些质数筛掉这个区间内的合数是有可能的。问题是最少需要多少质数才能保证一定可以筛掉这个区间内的所有合数。
根据\(n\)的质因子中最多只有一个大于\(\sqrt[2]{n}\)的性质,对于一个合数n,我们使用\(\sqrt[2]{n}\)内的质数一定可以筛掉它。n即使取最大值\(2^{31}−1\),我们也仅仅需要到\(50000\)内的所有质数即可保证所有合数一定会被筛除,而线性筛解决1到50000内的质数是很轻松的。

算法流程

  1. 首先采用线性筛找到1到50000内的所有质数
  2. 遍历步骤1找到的所有质数,对于每个质数p,我们将[L, R]内p的所有倍数筛除
  3. 提取出[L, R]内的所有质数,寻找距离最小和最大的质数对

算法细节
在算法流程2中涉及到一些技巧

  • 技巧1:如何找到>=L的最小的p的倍数\(p_0\)
    i. \(L = k * p\),那么 \(p_0 = \frac{L}{p} * p\),例如>=4的最小的2的倍数为4
    ii. \(L = k * p + c,(0 < c < p)\),那么 \(p_0 = (\frac{L}{p} + 1) * p\),例如>=5的最小的2的倍数为6
    将以上两种情况合并,可以得到\(p_0 = \lceil \frac{L}{p} \rceil * p\)

  • 技巧2:如何不采用ceil()实现上取整
    \(\lceil \frac{L}{p} \rceil = \lfloor \frac{L + p - 1}{p} \rfloor\)

    i.\(L = k * p\),那么 \(\lceil \frac{L}{p} \rceil = k\)
    \(\lfloor \frac{L + p - 1}{p} \rfloor = \lfloor \frac{k * p + p - 1}{p} \rfloor = \lfloor k + \frac{p - 1}{p} \rfloor\) = k。(\(\frac{p - 1}{p}\)下取整时等于0)

    ii. \(L = k * p + c,(0 < c < p)\),那么 \(\lceil \frac{L}{p} \rceil = \lceil k + \frac{c}{p} \rceil = k + 1\)。(\(\frac{c}{p}\)上取整等于1);
    \(\lfloor \frac{L + p - 1}{p} \rfloor = \lfloor k + 1 + \frac{c - 1}{p} \rfloor\) = k + 1。(\(\frac{c - 1}{p}\)下取整时等于0)

代码实现
以以下题目为例,该题的数据范围满足上述的特征

给定两个整数 \(L\)\(U\),你需要在闭区间 \([L,U]\) 内找到距离最接近的两个相邻质数 \(C_1\)\(C_2\)(即 \(C_2−C_1\) 是最小的),如果存在相同距离的其他相邻质数对,则输出第一对。
同时,你还需要找到距离最远的两个相邻质数 \(D_1\)\(D_2\)(即 \(D_1−D_2\) 是最大的),如果存在相同距离的其他相邻质数对,则输出第一对。
\(L\)\(U\) 的差值不会超过 \(10^6\)
\(1 ≤ L < U ≤ 2^{31} − 1\)

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>

using namespace std;

typedef long long LL;

const int N = 1e6 + 10, M = 50010;

int primes[M], cnt;
int st[N]; // 需要标记区间每一个数,由于数值过大所以采用偏移量,最大为1e6
int primes_res[M], st_res[N];

void init(int n)
{
    for (int i = 2; i <= n; ++ i)
    {
        if (!st[i]) primes[cnt ++] = i;
        for (int j = 0; primes[j] * i <= n; ++ j)
        {
            st[primes[j] * i] = true;
            if (i % primes[j] == 0) break;
        }
    }
}
/**
 * 代码易错点
 * 1. 多组输入st_res数组忘记初始化
 * 2. 筛掉[L, R]内的所有质数时,起点的选择,容易忽略1 * p的情况
 * 3. 标记[L, R]内的素数时,容易忘记需要使用偏移量
 * 4. 筛法无法筛除1,所以判断哪些数是素数时如果不能保证区间内一定不存在1,则一定要特判
 * 5. 在提取素数循环时,因数据最大会到INT_MAX,所以如果数据定为int,最后一次循环会爆int,造成死循环
 */
int main()
{
    init(50000);
    int l, r;
    while (cin >> l >> r)
    {
        memset(st_res, false, sizeof st_res); // 多组输入
        // 遍历每一个质数p,将[L, R]内的p的倍数筛除
        for (int i = 0; i < cnt; ++ i)
        {
            LL p = primes[i];
            // >=L的最小的p的倍数,当p=2,l=2时,(l + p - 1LL) / p * p = 2,相当于筛掉了1 * p,但p的倍数最小应该是2*p,所以这里取最大就是为了解决1*p的问题
            for (LL j = max(2 * p, (l + p - 1) / p * p); j <= r; j += p) // l + p - 1可能会爆int,所以选择将p变为LL,不能仅变1因为max函数需要保持前后类型相同
                st_res[j - l] = true; // 因为j最大是2147483647,这里需要使用偏移量
        }
        
        // // 将[L, R]内的合数筛除后,需要将素数全部提取出来用以方便查看相邻的素数
        int size = 0;
        for ( LL i = l; i <= r; ++ i) // 易错点5
            if (!st_res[i - l] && i >= 2) // 对应上方的偏移量,筛法是无法筛掉1的,对应易错点4
                primes_res[size ++] = i;
        // 也可以直接考虑偏移量,这样不会出现循环爆int的问题
        // int size = 0;
        // for (int i = 0; i <= r - l; ++ i)
        //     if (!st_res[i] && i + l >= 2)
        //         primes_res[size ++] = i + l;
                
        // 寻找距离最近和最远的相邻的质数
        int minp = 0, maxp = 0; // 因为每面对一组素数,我们都需要将其距离与目前距离最小最大的素数对进行比较,可以使用一个变量存储距离,另两个变量存储下标,但最简便的方式是存储距离最小最大的两个素数对中前一个数的下标
        for (int i = 0; i + 1 < size; ++ i)
        {
            int d = primes_res[i + 1] - primes_res[i];
            if (d < primes_res[minp + 1] - primes_res[minp]) minp = i;
            if (d > primes_res[maxp + 1] - primes_res[maxp]) maxp = i;
        }
        
        if (size < 2) printf("There are no adjacent primes.\n"); // 小于两个质数,不会存在一个素数对的
        else printf("%d,%d are closest, %d,%d are most distant.\n", primes_res[minp], primes_res[minp + 1], primes_res[maxp], primes_res[maxp + 1]);
    }
    
    return 0;
}
posted @ 2021-01-31 21:13  0x7F  阅读(250)  评论(0编辑  收藏  举报