2021.2.3

2021 年 2 月 3 日 星期三(已编辑)
1
这篇文章上次修改于 2021 年 2 月 3 日 星期三,可能部分内容已经不适用,如有疑问可询问作者。

2021.2.3

数论

素数

简介

一个大于 1 的自然数,除了 1 和它自身外,不能被其他自然数整除的数叫做素数;否则称为合数

  • 1既不是质数也不是合数

判断方法

暴力 · O(n2)O(\frac{n}{2})

bool isPrime(a)
{
    if (a < 2) return 0;
    for (int i = 2; i * i < a; ++i)
        if (!(a % i)) return 0;
    return 1;
}

Miller-Rabin 素性测试 · O(k log3n)O(k \ log^{3}n)

Fermat 素性测试

我们可以根据费马小定理得出一种检验素数的思路:

它的基本思想是不断地选取在 [2, n - 1] 中的基 a,并检验是否每次都有 an1  1 (mod n)a^{n - 1} \ \equiv \ 1 \ (mod \ n)

bool millerRabin(int n)
{
    if (n < 3) return n == 2;
    // test_time 为测试次数,建议设为不小于 8
    // 的整数以保证正确率,但也不宜过大,否则会影响效率
    for (int i = 1; i <= test_time; ++i)
    {
        int a = rand() % (n - 2) + 2;
        if (quickPow(a, n - 1, n) != 1) return 0;
    }
    return 1;
}

很遗憾,费马小定理的逆定理并不成立,换言之,满足了 an1  1 (mod n)a^{n - 1} \ \equiv \ 1 \ (mod \ n),a 也不一定是素数。

卡迈克尔数

上面的做法中随机地选择 a,很大程度地降低了犯错的概率。但是仍有一类数,上面的做法并不能准确地判断。

对于合数 n,如果对于所有正整数 a,a 和 n 互素,都有同余式 an1  1 (mod n)a^{n - 1} \ \equiv \ 1 \ (mod \ n) 成立,则合数 n 为卡迈克尔数(Carmichael Number),又称为费马伪素数。

比如,561 = 3 x 11 x 17 就是一个卡迈克尔数。

而且我们知道,若 n 为卡迈克尔数,则 m = 2n - 1 也是一个卡迈克尔数,从而卡迈克尔数的个数是无穷的。

二次探测定理

如果 p 是奇素数,则 x2  1 (mod p)x^{2} \ \equiv \ 1 \ (mod \ p) 的解为 x  1 (mod p)x \ \equiv \ 1 \ (mod \ p) 或者 x  p1 (mod p)x \ \equiv \ p-1 \ (mod \ p)

要证明该定理,只需将上面的方程移项,再使用平方差公式,得到 (x+1)(x1)  0 (mod p)(x + 1)(x - 1) \ \equiv \ 0 \ (mod \ p),即可得出上面的结论。

实现

根据卡迈克尔数的性质,可知其一定不是 pe

不妨将费马小定理和二次探测定理结合起来使用:

an1  1 (mod n)a^{n - 1} \ \equiv \ 1 \ (mod \ n) 中的指数 n - 1 分解为 n  1 = u × 2tn \ - \ 1 \ = \ u \ \times \ 2^{t},在每轮测试中对随机出来的 a 先求出 au (mod n)a^{u} \ (mod \ n),之后对这个值执行最多 t 次平方操作,若发现非平凡平方根时即可判断出其不是素数,否则通过此轮测试。

模板
bool millerRabbin(int n)
{
    if (n < 3) return n == 2;
    int a = n - 1, b = 0;
    while (!(a % 2))
    {
        a /= 2;
        ++b;
    }
    // test_time 为测试次数,建议设为不小于 8 的整数以保证正确率,但也不宜过大,否则会影响效率
    for (int i = 1, j; i <= test_time; ++i)
    {
        int x = rand() % (n - 2) + 2, v = quickPow(x, a, n);
        if (v == 1 || v == n - 1) continue;
        for (j = 0; j < b; ++j)
        {
            v = (long long)v * v % n;
            if (v == n - 1) break;
        }
        if (j >= b) return 0;
    }
    return 1;
}

最大公约数 · GCD

简介

最大公约数即为 Greatest Common Divisor,常缩写为 gcd。

在素数一节中,我们已经介绍了约数的概念。

一组数的公约数,是指同时是这组数中每一个数的约数的数。而最大公约数,则是指所有公约数里面最大的一个。

求法

欧几里得算法 · O(logn)O(\log n)

int gcd(int a, int b)
{
    if (!b) return a;
    return gcd(b, a % b);
}

多个数的最大公约数

答案一定是每个数的约数,那么也一定是每相邻两个数的约数。我们采用归纳法,可以证明,每次取出两个数求出答案后再放回去,不会对所需要的答案造成影响。

int a[MAXN];
int multi_gcd(int n)
{
    int tmp1, tmp2;
    tmp1 = a[1];
    for (int i = 2; i <= n; i++)
    {
        tmp2 = a[i];
        a[i] = gcd(tmp1, tmp2);
        tmp1 = a[i];
    }

    return a[n];
}

最小公倍数 · LCM

int lcm(int a, int b)
{
    return a / gcd(a, b) * b;    // 先除再乘避免溢出
}

多个数的最小公倍数

可以发现,当我们求出两个数的 gcd 时,求最小公倍数是 O(1)O(1) 的复杂度。那么对于多个数,我们其实没有必要求一个共同的最大公约数再去处理,最直接的方法就是,当我们算出两个数的 gcd,或许在求多个数的 gcd 时候,我们将它放入序列对后面的数继续求解,那么,我们转换一下,直接将最小公倍数放入序列即可。

int a[MAXN];
int multi_lcm(int n)
{
    int tmp1, tmp2;
    tmp1 = a[1];
    for (int i = 2; i <= n; i++)
    {
        tmp2 = a[i];
        a[i] = lcm(tmp1, tmp2);
        tmp1 = a[i];
    }

    return a[n];
}

欧拉函数

定义

欧拉函数(Euler's totient function),即 ϕ(n)\phi(n),表示的是小于等于 n 和 n 互质的数的个数。

比如说 ϕ(1)=1\phi(1) = 1

当 n 是质数的时候,显然有 ϕ(n)=n1\phi(n) = n - 1

性质

  • 欧拉函数是积性函数。

    积性是什么意思呢?如果有 gcd(a, b) = 1gcd(a, \ b) \ = \ 1,那么 ϕ(a×b)=ϕ(a)×ϕ(b)\phi(a \times b) = \phi(a) \times \phi(b)

    特别地,当 n 是奇数时 ϕ(2n)=ϕ(n)\phi(2n) = \phi(n)

  • n=dnϕ(d)n = \sum_{d|n} \phi(d)
  • 若 n = pk,其中 p 是质数,那么 ϕ(n)=pkpk1\phi(n) = p^{k} - p^{k-1}

  • 由唯一分解定理,设 n=Πi=1npikin = \Pi_{i=1}^{n}p_{i}^{k_{i}},其中 pip_{i}是质数,有 ϕ(n)=n×Πi=1spi1pi\phi(n) = n \times \Pi_{i=1}^{s} \frac{p_{i} - 1}{p_{i}}

求欧拉函数值

int euler_phi(int n)
{
    int m = int(sqrt(n + 0.5));
    int ans = n;
    for (int i = 2; i <= m; i++)
        if (n % i == 0)
        {
            ans = ans / i * (i - 1);
            while (n % i == 0) n /= i;
        }
    if (n > 1) ans = ans / n * (n - 1);
    return ans;
}

如果将上面的程序改成如下形式,会提升一点效率:

int euler_phi(int n)
{
    int ans = n;
    for (int i = 2; i * i <= n; i++)
        if (n % i == 0)
        {
            ans = ans / i * (i - 1);
            while (n % i == 0) n /= i;
        }
    if (n > 1) ans = ans / n * (n - 1);
    return ans;
}

欧拉定理

gcd(a, m) = 1gcd(a, \ m) \ = \ 1aϕ(m)1(modm)a^{\phi(m)} \equiv 1 (mod m)

扩展欧拉定理

ab{ab mod ϕ(p),gcd(a, p) = 1ab,gcd(a, p)  1, b < ϕ(p) (mod p)ab mod ϕ(p)+ϕ(p),gcd(a, b)  1, b  ϕ(p) a^{b} \equiv \left\{ \begin{aligned} a^{b \ mod \ \phi(p)} & , & gcd(a, \ p) \ = \ 1 \\ a^{b} & , & gcd(a, \ p) \ \neq \ 1 & , \ b \ < \ \phi(p) \ (mod \ p)\\ a^{b \ mod \ \phi(p)+\phi(p)} & , & gcd(a, \ b) \ \neq \ 1 & , \ b \ \geq \ \phi(p) \end{aligned} \right.

筛法

素数筛法

如果我们想要知道小于等于 n 有多少个素数呢?

一个自然的想法是对于小于等于 n 的每个数进行一次质数检验。这种暴力的做法显然不能达到最优复杂度。

埃拉托斯特尼(Eratosthenes)筛法 · O(nloglogn)O(n\log\log n)

如果 x 是合数,那么 x 的倍数也一定是合数。利用这个结论,我们可以避免很多次不必要的检测。

如果我们从小到大考虑每个数,然后同时把当前这个数的所有(比自己大的)倍数记为合数,那么运行结束的时候没有被标记的数就是素数了。

int Eratosthenes(int n)
{
    int p = 0;
    for (int i = 0; i <= n; ++i) is_prime[i] = 1;
    is_prime[0] = is_prime[1] = 0;
    for (int i = 2; i <= n; ++i)
    {
        if (is_prime[i])
        {
            prime[p++] = i;  // prime[p]是i,后置自增运算代表当前素数数量
            if ((long long)i * i <= n)
                for (int j = i * i; j <= n; j += i)
                    // 因为从 2 到 i - 1 的倍数我们之前筛过了,这里直接从 i
                    // 的倍数开始,提高了运行速度
                    is_prime[j] = 0;  // 是i的倍数的均不是素数
        }
    }
    return p;
}
筛至平方根 && 只筛奇数

显然,要找到直到 n 为止的所有素数,仅对不超过 n\sqrt{n} 的素数进行筛选就足够了。

因为除 2 以外的偶数都是合数,所以我们可以直接跳过它们,只用关心奇数就好。

int n;
vector<char> is_prime(n + 1, true);
is_prime[0] = is_prime[1] = false;
for (int i = 2; i * i <= n; i++)
{
    if (!(i % 2)) continue;
    if (is_prime[i])
    {
        for (int j = i * i; j <= n; j += i) is_prime[j] = false;
    }
}

线性筛法

埃氏筛法仍有优化空间,它会将一个合数重复多次标记。有没有什么办法省掉无意义的步骤呢?答案是肯定的。

如果能让每个合数都只被标记一次,那么时间复杂度就可以降到 O(n)O(n)

void init()
{
    phi[1] = 1;
    for (int i = 2; i < MAXN; ++i)
    {
        if (!vis[i])
        {
            phi[i] = i - 1;
            pri[cnt++] = i;
        }
        for (int j = 0; j < cnt; ++j)
        {
            if (1ll * i * pri[j] >= MAXN) break;
            vis[i * pri[j]] = 1;
            if (i % pri[j])
            {
                phi[i * pri[j]] = phi[i] * (pri[j] - 1);
            }
            else
            {
                // i % pri[j] == 0
                // 换言之,i 之前被 pri[j] 筛过了
                // 由于 pri 里面质数是从小到大的,所以 i 乘上其他的质数的结果一定也是
                // pri[j] 的倍数 它们都被筛过了,就不需要再筛了,所以这里直接 break
                // 掉就好了
                phi[i * pri[j]] = phi[i] * pri[j];
                break;
            }
        }
    }
}
  • prii 个素数
  • phi~ i 中与 i 互质的数的个数

筛法求欧拉函数

void phi_table(int n, int* phi)
{
    for (int i = 2; i <= n; i++) phi[i] = 0;
    phi[1] = 1;
    for (int i = 2; i <= n; i++)
        if (!phi[i])
            for (int j = i; j <= n; j += i)
            {
                if (!phi[j]) phi[j] = j;
                phi[j] = phi[j] / i * (i - 1);
            }
}

筛法求约数个数

d的约数个数

num的最小质因子出现次数

定理:若 n = i=1mpicin \ = \ \prod_{i=1}^{m} p_{i}^{c_{i}}di = i=1mci + 1d_{i} \ = \ \prod_{i=1}^{m}c_{i} \ + \ 1

void pre()
{
    d[1] = 1;
    for (int i = 2; i <= n; ++i)
    {
        if (!v[i]) v[i] = 1, p[++tot] = i, d[i] = 2, num[i] = 1;
        for (int j = 1; j <= tot && i <= n / p[j]; ++j)
        {
            v[p[j] * i] = 1;
            if (i % p[j] == 0)
            {
                num[i * p[j]] = num[i] + 1;
                d[i * p[j]] = d[i] / num[i * p[j]] * (num[i * p[j]] + 1);
                break;
            }
            else
            {
                num[i * p[j]] = 1;
                d[i * p[j]] = d[i] * 2;
            }
        }
    }
}

筛法求约数和

f的约数和

g的最小质因子的 p + p1 + p2 +  + pkp \ + \ p^{1} \ + \ p^{2} \ + \ \cdots \ + \ p^{k}

void pre()
{
    g[1] = f[1] = 1;
    for (int i = 2; i <= n; ++i)
    {
        if (!v[i]) v[i] = 1, p[++tot] = i, g[i] = i + 1, f[i] = i + 1;
        for (int j = 1; j <= tot && i <= n / p[j]; ++j)
        {
            v[p[j] * i] = 1;
            if (i % p[j] == 0)
            {
                g[i * p[j]] = g[i] * p[j] + 1;
                f[i * p[j]] = f[i] / g[i] * g[i * p[j]];
                break;
            }
            else
            {
                f[i * p[j]] = f[i] * f[p[j]];
                g[i * p[j]] = 1 + p[j];
            }
        }
    }
    for (int i = 1; i <= n; ++i) f[i] = (f[i - 1] + f[i]) % Mod;
}

乘法逆元

简介

如果一个线性同余方程 ax  1 (mod b)ax \ \equiv \ 1 \ (mod \ b) ,则 x 称为 a mod b 的逆元,记作 a1a^{-1}

如何求逆元

扩展欧几里得法

void exgcd(int a, int b, int& x, int& y)
{
    if (!b)
    {
        x = 1;
        y = 0;
        return;
    }
    exgcd(b, a % b, y, x);
    y -= a / b * x;
}

快速幂法

因为 ax  1 (mod b)ax \ \equiv \ 1 \ (mod \ b)

所以 ax  ab1 (mod b)ax \ \equiv \ a^{b-1} \ (mod \ b)(根据费马小定理);

所以 x  ab2 (mod b)x \ \equiv \ a^{b-2} \ (mod \ b)

然后我们就可以用快速幂来求了。

inline int qpow(long long a, int b)
{
    int ans = 1;
    a = (a % p + p) % p;
    for (; b; b >>= 1)
    {
        if (b & 1) ans = (a * ans) % p;
        a = (a * a) % p;
    }
    return ans;
}
  • 使用费马小定理需要限制 b 是一个素数,而扩展欧几里得算法只要求 gcd(a, p) = 1\gcd(a, \ p) \ = \ 1

线性求逆元

inv[1] = 1;
for (int i = 2; i <= n; ++i)
{
    inv[i] = (long long)(p - p / i) * inv[p % i] % p;
}

线性求任意 n 个数的逆元

s[0] = 1;
for (int i = 1; i <= n; ++i) s[i] = s[i - 1] * a[i] % p;
sv[n] = qpow(s[n], p - 2);
// 当然这里也可以用 exgcd 来求逆元,视个人喜好而定.
for (int i = n; i >= 1; --i) sv[i - 1] = sv[i] * a[i] % p;
for (int i = 1; i <= n; ++i) inv[i] = sv[i] * s[i - 1] % p;

中国剩余定理

简介

中国剩余定理 (Chinese Remainder Theorem, CRT) 可求解如下形式的一元线性同余方程组(其中 n1, n2, , nkn_{1}, \ n_{2}, \ \cdots, \ n_{k} 两两互质): \left { \begin{array}{c} x \ &\equiv \ &a{1} \ (mod \ n{1}) \ x \ &\equiv \ &a{2} \ (mod \ n{2}) \ &\cdots\ \ x \ &\equiv \ &a{k} \ (mod \ n{k}) \end{array} \right .

算法流程

  1. 计算所有模数的积 n;
  2. 对于第 i 个方程:
    1. 计算 mi = nnim_{i} \ = \ \frac{n}{n_{i}}
    2. 计算 mim_{i} 在模 nin_{i} 意义下的逆元 mi1m_{i}^{-1}
    3. 计算 ci = mimi1c_{i} \ = \ m_{i}m_{i}^{-1}不要对 nin_{i} 取模)。
  3. 方程组的唯一解为:a = i=1kaici (mod n)a \ = \ \sum_{i=1}^{k}a_{i}c_{i} \ (mod \ n)

使用社交账号登录

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...