2021.2.3
数论
素数
简介
一个大于 1 的自然数,除了 1 和它自身外,不能被其他自然数整除的数叫做素数;否则称为合数
- 1既不是质数也不是合数
判断方法
暴力 ·
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 素性测试 ·
Fermat 素性测试
我们可以根据费马小定理得出一种检验素数的思路:
它的基本思想是不断地选取在 [2, n - 1] 中的基 a,并检验是否每次都有
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;
}
很遗憾,费马小定理的逆定理并不成立,换言之,满足了 ,a 也不一定是素数。
卡迈克尔数
上面的做法中随机地选择 a,很大程度地降低了犯错的概率。但是仍有一类数,上面的做法并不能准确地判断。
对于合数 n,如果对于所有正整数 a,a 和 n 互素,都有同余式 成立,则合数 n 为卡迈克尔数(Carmichael Number),又称为费马伪素数。
比如,561 = 3 x 11 x 17 就是一个卡迈克尔数。
而且我们知道,若 n 为卡迈克尔数,则 m = 2n - 1 也是一个卡迈克尔数,从而卡迈克尔数的个数是无穷的。
二次探测定理
如果 p 是奇素数,则 的解为 或者 。
要证明该定理,只需将上面的方程移项,再使用平方差公式,得到 ,即可得出上面的结论。
实现
根据卡迈克尔数的性质,可知其一定不是 pe。
不妨将费马小定理和二次探测定理结合起来使用:
将 中的指数 n - 1 分解为 ,在每轮测试中对随机出来的 a 先求出 ,之后对这个值执行最多 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。
在素数一节中,我们已经介绍了约数的概念。
一组数的公约数,是指同时是这组数中每一个数的约数的数。而最大公约数,则是指所有公约数里面最大的一个。
求法
欧几里得算法 ·
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 时,求最小公倍数是 的复杂度。那么对于多个数,我们其实没有必要求一个共同的最大公约数再去处理,最直接的方法就是,当我们算出两个数的 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 和 n 互质的数的个数。
比如说 。
当 n 是质数的时候,显然有 。
性质
欧拉函数是积性函数。
积性是什么意思呢?如果有 ,那么 。
特别地,当 n 是奇数时 。
若 n = pk,其中 p 是质数,那么 。
由唯一分解定理,设 ,其中 是质数,有 。
求欧拉函数值
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;
}
欧拉定理
若 则
扩展欧拉定理
筛法
素数筛法
如果我们想要知道小于等于 n 有多少个素数呢?
一个自然的想法是对于小于等于 n 的每个数进行一次质数检验。这种暴力的做法显然不能达到最优复杂度。
埃拉托斯特尼(Eratosthenes)筛法 ·
如果 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 为止的所有素数,仅对不超过 的素数进行筛选就足够了。
因为除 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;
}
}
线性筛法
埃氏筛法仍有优化空间,它会将一个合数重复多次标记。有没有什么办法省掉无意义的步骤呢?答案是肯定的。
如果能让每个合数都只被标记一次,那么时间复杂度就可以降到 了
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的最小质因子出现次数
定理:若 则
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的最小质因子的
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;
}
乘法逆元
简介
如果一个线性同余方程 ,则 x 称为 a mod b 的逆元,记作 。
如何求逆元
扩展欧几里得法
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;
}
快速幂法
因为 ;
所以 (根据费马小定理);
所以 。
然后我们就可以用快速幂来求了。
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 是一个素数,而扩展欧几里得算法只要求 。
线性求逆元
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) 可求解如下形式的一元线性同余方程组(其中 两两互质): \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 .
算法流程
- 计算所有模数的积 n;
- 对于第 i 个方程:
- 计算 ;
- 计算 在模 意义下的逆元
- 计算 (不要对 取模)。
- 方程组的唯一解为:。