Featured image of post 埃式筛,快速幂,gcd和lcm,乘法逆元

埃式筛,快速幂,gcd和lcm,乘法逆元

请输入文字描述

前言

学习网站:Starrycoding

常用算法wiki:OI wiki

埃式筛

埃式筛是一种用于快速找到一定范围内所有质数的算法。它的基本思想是从2开始,将每个质数的倍数都标记成合数,直到筛完所有小于等于n的数。

具体来说,算法的步骤如下:

  1. 初始化一个vis数组,将所有元素标记为0,刚开始我们认为所有数都是质数。而1代表不是质数。然后我们初始化将0,1标记为1。
  2. 从2开始判断是不是质数,如果是质数,就将它的倍数都标记为1.
  3. 然后我们判断下一个数是不是质数,如果是质数,就将它的倍数都标记为1.

过程1

最终vis数组中为0的数就是质数。

过程2

例题

P43 【模板】埃氏筛法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <bits/stdc++.h>
using namespace std;
const int N = 2e6 + 9;
bitset<N> vis;

int main()
{
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    int n;
    cin >> n;
    vis[0] = true;
    vis[1] = true;
    for (int i = 2; i <= n; i++)
    {
        if (!vis[i])
        {
            for (int j = i * 2; j <= n; j += i)  // 从i的2倍开始,每次加i
            {
                vis[j] = true;
            }
        }
    }
    for (int i = 2; i <= n; i++)
    {
        if (!vis[i])
        {
            cout << i << " ";
        }
    }
    cout << "\n";
    return 0;
}

我们可以分析下这个算法的时间复杂度。

如图,我们可以看到,埃式筛枚举倍数的方法使得这个循环的时间复杂度为$O(n\log n)$。于是总共就是$O(n\log\log n)$。

顺带一提,枚举倍数我们也可以优化,将循环改为这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
for (int i = 2; i <= n; i++)
    {
        if (!vis[i])
        {
            for (ll j = 1ll*i * i; j <= n; j += i)
            {
                vis[j] = true;
            }
        }
    }

初始化j为i的平方,不会影响结果(我们可以自己代数字看一下,假设i很大,那么在到i之前,$2\times i$,$3\times i$,$4\times i$等倍数肯定已经在枚举到$2,3$等数的时候被标记过了)。

最终代码就是:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <bits/stdc++.h>
using namespace std;
const int N = 2e6 + 9;
typedef long long ll;
bitset<N> vis;

int main()
{
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    int n;
    cin >> n;
    vis[0] = true;
    vis[1] = true;
    for (int i = 2; i <= n; i++)
    {
        if (!vis[i])
        {
            for (ll j = 1ll*i * i; j <= n; j += i)
            {
                vis[j] = true;
            }
        }
    }
    for (int i = 2; i <= n; i++)
    {
        if (!vis[i])
        {
            cout << i << " ";
        }
    }
    cout << "\n";
    return 0;
}

gcd和lcm

gcd是最大公因数,lcm是最小公倍数。

关于gcd和lcm,我们首要要知道一个定理,叫做惟一分解定理

该定理断言:任何一个大于1的整数n都可以分解成若干个素因数的连乘积,如果不计各个素因数的顺序,那么这种分解是惟一

$$N={p_1}^{a_1} \times {p_2}^{a_2} \times {p_3}^{a_3} \times {p_4}^{a_4} ....\times {p_m}^{a_m} $$

其中$p_i$是素数,$a_i$是正整数。

而gcd和lcm也与这个定理有关。

如图,将两个数的质因数分解,保证他们的因子全部相同,那么他们的gcd就是这些因子的最小值乘积,而lcm就是这些因子的最大值乘积。

同时,我们可以发现$gcd(a,b)\times lcm(a,b)=a\times b$。

于是,我们得出了gcd与lcm的关系,那么我们只要求出gcd或lcm,另一个就可以通过公式得出。

通常,我们会使用辗转相除法来求gcd。然后再得出lcm。

辗转相除法的代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

ll gcd(ll a, ll b)
{
    if (b == 0)
    {
        return a;
    }
    else
    {
        return gcd(b, a % b);
    }
}

例题

P65 【模板】gcd和lcm

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

ll gcd(ll a, ll b)
{
    if (b == 0)
    {
        return a;
    }
    else
    {
        return gcd(b, a % b);
    }
}

ll lcm(ll a, ll b)
{
    return (a / gcd(a, b)) * b;  // 注意这里要先除再乘,防止溢出
}

void solve()
{
    ll a, b;
    cin >> a >> b;
    cout << gcd(a, b) << " " << lcm(a, b) << "\n";
}

int main()
{
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    int t;
    cin >> t;
    while (t--)
    {
        solve();
    }
}

快速幂

快速幂是一种用于计算幂运算的算法,它可以在$O(\log n)$的时间复杂度内计算出$a^n$的值。

快速幂的基本思想是将幂运算转化为二进制数的形式,然后利用二进制数的性质来计算幂运算。

如图,假设我们要计算$a^n$,我们可以将n转化为二进制数,然后将二进制数转化为幂运算的形式。

例如,$a^7$可以转化为$a^4\times a^2\times a^1$。

7的二进制为111,我们对应只要有1就乘上a,并且数字a每次都要自乘,这样就可以得到$a^7$。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
ll qmi(ll a, ll b, ll c)
{
    ll res = 1;
    while (b != 0)
    {
        if (b & 1) // 如果b的二进制最后一位为1
        {
            res = res * a;
        }
        a = a * a; // 每次循环a都要翻倍
        b >>= 1;
    }
    return res;
}

需要注意的是,假如题目有运用快速幂,那么题目绝对是要取模的,因为出题人肯定要想办法避免数据溢出。

这里要指出取模运算对于$+-\times$这三种运算是封闭的,也就是说,$(a+b)%m=(a%m+b%m)%m$,$(a\times b)%m=(a%m\times b%m)%m$。

我们可以看出在最终结果需要取模时,我们可以将每一步都取模,却不会影响最终结果

例题

P66 【模板】快速幂

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

ll qmi(ll a, ll b, ll c)
{
    ll res = 1;
    while (b != 0)
    {
        if (b & 1) // 如果b的二进制最后一位为1
        {
            res = res * a % c;
        }
        a = a * a % c; // 每次循环a都要翻倍
        b >>= 1;
    }
    return res;
}

void solve()
{
    ll a, b, c;
    cin >> a >> b >> c;
    cout << qmi(a, b, c) << "\n";
}

int main(void)
{
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    int t;
    cin >> t;
    while (t--)
    {
        solve();
    }
    return 0;
}

乘法逆元

乘法逆元是数论里的知识

假设一个方程为$\frac{1}{a} \bmod b = x$

我们可以将其转化为$x\times a \bmod b = 1$。

其中的这个$x$为a的逆元($\bmod b$ 意义下)

$$\frac{a\times b}{c} \mod d = (a\times b \times inv(c)) \mod d$$

其中的$inv(c)$为c的逆元。

需要注意的是,这个逆元绝对是要有取模参与的。

接着我们还要知道费马小定理

费马小定理告诉我们

当$p$为质数时(任意一个质数都行),始终有

$$a^{p-1} \bmod p = 1$$

则有

$$a^{p-1} \equiv 1 \pmod{p} $$$$a \times a^{p-2} \equiv 1 \pmod{p} $$$$a^{p-2} \equiv \frac{1}{a} \pmod{p} $$$$a^{p-2} \equiv inv(a) \pmod{p} $$

所以,我们就可以用快速幂加上取模来求逆元了(很明显,要求逆元题目就会规定一个质数来让你取模,常见的有$998244353$,$1e9+7$)

例题

P44 【模板】乘法逆元

分析:

如图,我们可以把

$$\frac{a\times x + b}{c\times x} $$

转化为

$$(a\times x +b) \times (c\times x)^{p-2} \bmod p$$

如此就可以求出题目的函数值,并且不会数据溢出

假如不用这种方法,有除法存在会导致我们没法使用取模来使数据不溢出

代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll p = 998244353;

ll qmi(ll a, ll b)
{
    ll res = 1;
    while (b != 0)
    {
        if (b & 1) // 如果b的二进制最后一位为1
        {
            res = res * a % p;
        }
        a = a * a % p; // 每次循环a都要翻倍
        b >>= 1;
    }
    return res;
}

ll inv(ll x)
{
    return qmi(x, p - 2);
}

ll f(ll x, ll a, ll b, ll c)
{
    return ((a * x % p + b) % p) * inv(c * x % p) % p;
}

void solve()
{
    ll a, b, c, q;
    cin >> a >> b >> c >> q;
    while (q--)
    {
        ll x;
        cin >> x;
        cout << f(x, a, b, c) << "\n";
    }
}

int main(void)
{
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    int t;
    cin >> t;
    while (t--)
    {
        solve();
    }
    return 0;
}
根据CC BY-NC-SA 4.0协议授权
最后更新于 2025-07-16
我等生来追逐辉光,一如火花向上飞舞
使用 Hugo 构建
主题 StackJimmy 设计