随便写写,不喜勿喷。
Prime
对于整数 n n n,若 n ≠ − 1 , 0 , 1 n \neq -1,0,1 n=−1,0,1,且 n n n 除去显然因数 ( ± n (\pm n (±n、 ± 1 ) \pm 1) ±1) 外没有其他因数,则我们称 n n n 为质数。
Primality test
又称 素性测试,
是一种判定某个数是否为质数的方法。
素性测试有两种,
1.确定性测试 \ 确定型算法
2.概率性测试 \ 随机型算法
确定性测试可绝对确定一个数是否为质数,概率性测试则有较小的概率错误的将合数判断为质数,因此,通过了概率性测试的数字被称为 可能质数,通过了概率性测试的合数被称为 伪质数,常见的有费马伪质数。
下面将简单带过一下,暴力测试(试除法),
引入费马小定理,以及两个基于费马小定理的随机型算法 费马素性测试、 M i l l e r \mathrm{Miller} Miller- R a b i n \mathrm{Rabin} Rabin 素性测试。
Trial Division
一个数的因子总是成对出现的,因此对于正整数 n n n,它的最小因子不会大于 n \sqrt n n,
若 2 ∼ n 2 \sim \sqrt n 2∼n 中不存在一个整数 k k k,使得 k ∣ n k \mid n k∣n,那么我们就可以确定的认为 n n n 就是质数。
bool is_prime(int n) {
for (int i = 2; i <= sqrt(n); ++i)
if (n % i == 0) return 0;
return 1;
}
Fermat’s little theorem
若 p p p 是质数, a a a 为整数且 a a a 不是 p p p 的倍数,则有: a p − 1 ≡ 1 ( m o d p ) a^{p-1}\equiv1 \pmod p ap−1≡1(modp)
费马小定理也是欧拉定理的特殊形式。
费马测试是最为简单的概率性测试,
其思想是,不断的在 2 ∼ n − 1 2 \sim n-1 2∼n−1 中,选择一个 a a a 作为基,判断对于整数 a a a,是否都有 a p − 1 ≡ 1 ( m o d p ) a^{p-1}\equiv1 \pmod p ap−1≡1(modp)。
int mod_pow(int a, int b, int p) {
int pow = 1;
while (b) {
if (b & 1) pow = pow * a % p;
a = a * a % p;
b >>= 1;
}
return pow;
}
int test_time = 50;
bool is_prime(int n) {
for (int i = 0; i < test_time; ++i)
if (mod_pow(1 + rand() % (n - 1), n - 1, n) != 1) return 0;
return 1;
}
在上面程序中,随机选择了 a a a,极大程度上避免了错误判定的发生,但有一类数无法被费马测试准确判断,
这类数被称为 卡迈克尔数,也被称为 费马伪素数,
其定义为对于合数 n n n,对于任意整数 a a a, gcd ( a , n ) = 1 \gcd (a,n)=1 gcd(a,n)=1 ,都有 a n − 1 ≡ 1 ( m o d n ) a^{n-1} \equiv 1 \pmod n an−1≡1(modn) 成立,则称这样的数为 卡迈克尔数,卡迈克尔数 有无穷多个,最小的 卡迈克尔数 是 561 561 561,这种数的存在也佐证了费马小定理的逆命题不成立。
不过 卡迈克尔数 的出现密度较低,在一亿以内的自然数中也仅仅只有 255 255 255 个。
Strong probable primes
若 p p p 为质数,则 x 2 ≡ 1 ( m o d p ) x^2 \equiv 1 \pmod p x2≡1(modp) 的解为 x ≡ ± 1 ( m o d p ) x \equiv\pm 1 \pmod p x≡±1(modp)。
但这个特性并非质数的精确表征,因此通过该式测试的数 p p p,被称为 强可能质数,而通过该式测试的合数则被称为 强伪质数。
截止至最后一次更新,搜索引擎上很多有关 米勒-拉宾 测试都未对该点讲解,所以这里简单介绍一下。
若 x ≢ ± a ( m o d p ) x \not\equiv\pm\sqrt a\pmod p x≡±a(modp),满足 x 2 ≡ a ( m o d p ) x^2 \equiv a\pmod p x2≡a(modp),则称 x x x 是以 p p p 为模的 a a a 的 非 平凡\显然 平方根。
特别地,当 p p p 为质数时, a a a 不存在非平凡平方根。
米勒-拉宾 素性测试就是将:费马小定理、二次探测定理、非显然平方根的性质结合起来。
具体地说:
给定一个整数 n n n,我们先判断它是否为偶数,若是则判断是它是否为 a a a,不是则存在一个 b b b, n = b × 2 k + 1 n = b ×2^k + 1 n=b×2k+1, k k k 尽可能的大,此时随机一个 a a a,根据费马小定理,若 n n n 为质数,则存在一个 k ′ < k k' < k k′<k,使得 a b ⋅ 2 k ′ ≡ ± 1 ( m o d n ) a^{b\cdot2^{k'}} \equiv \pm 1\pmod n ab⋅2k′≡±1(modn) 成立,否则 a a a 存在一个模 n n n 下的非显然平方根,此时 n n n 必为合数。
一次 米勒-拉宾 素性测试的误判在 1 4 \frac 14 41,因此建议对于每个数的测试次数都在 8 8 8 次以上。
关于上述概率的证明,等我实在是太闲的时候补。
bool is_prime(int n) {
if ((n & 1) == 0) return n == 2;
int b = n - 1, k = 0, j;
while (b & 1 == 0) b >>= 1, ++k;
for (int i = 0; i < test_time; ++i) {
int a = rand() % (n - 1) + 1;
int v = mod_pow(a, b, n);
if (v == 1) continue;
for (j = 0; j < k; ++j) {
if (v == n - 1) break;
v = v * v % n;
}
if (j == b) return 0;
}
return 1;
}
在程序设计竞赛中,算术基本定理表明,我们可以将任意一个大于 1 1 1 自然数表示为若干个质数的乘积,即 N = p 1 a 1 ⋅ p 2 a 2 ⋅ p 3 a 3 ⋅ ⋯ ⋅ p s s 1 = ∏ i = 1 s p i a i N=p^{a_1}_1\cdot p^{a_2}_2\cdot p^{a_3}_3\cdot \cdots \cdot p^{s_1}_s =\prod_{i=1}^sp^{a_i}_i N=p1a1⋅p2a2⋅p3a3⋅⋯⋅pss1=i=1∏spiai
这是很多带有数论标签的题目的突破口,
因此,掌握如何快速分解一个整数在程序设计竞赛中尤为的重要。
既然试除法可以通过 1 ∼ N 1 \sim \sqrt N 1∼N 之间是否存在 N N N 的因数来判断 N N N 是否为质数,那么同样的,
在试除法的基础上稍作修改,就能计算出 N N N 的所有质因子。
std::map<int, int> factor(int N) {
std::map<int, int> factors;
for (int p = 2; p <= sqrt(N); ++p)
if (N % p == 0) {
int k = 0;
while (N % p == 0) N /= p, ++k;
factors[p] += k;
}
if (N != 1) ++factors[N];
return factors;
}
朴素的试除法复杂度在 O ( N ) O(\sqrt N) O(N),若在有质数表的情况下试除则复杂度降至 O ( 2 N log N ) O(\cfrac{2\sqrt N}{\log N}) O(logN2N)。
关于 质数打表 点这里。
Pollard’s Rho 分解质因数的期望效率是反直觉的,因此在介绍 波拉德的 ρ \rho ρ 之前需要先引入生日悖论。
原命题我是在《费马大定理》上看到的,但现在书不在手边,就在网上随便搜了一个:
在一场英式足球赛里,通常会有 23 个人在赛场上:两支参赛队伍各有 11 名球员,外加 1 名裁判。在这23 人里,2 人或 2 人以上具有相同生日的概率是多少?
答案是约为 50 50 50%,这对于大多数没有接触过概率论的人来说都是反直觉的。
针对没有接触过概率的读者,这里简单验证一下:
设 A A A 为事件 n n n 个人生日不同, P ( A ) P(A) P(A) 为事件 A A A 发生的概率,则 A A A 的逆命题 A ˉ \bar A Aˉ, n n n 个人中至少有两个人生日相同的概率为 P ( A ˉ ) = 1 − P ( A ) P(\bar A) = 1 - P(A) P(Aˉ)=1−P(A)。 P ( A ) = 365 365 × 365 − 1 365 × 365 − 2 365 × ⋯ × 365 − n + 1 365 = ∏ i = 0 n − 1 365 − i 365 P(A) = \frac{365}{365} × \frac{365 - 1}{365} × \frac{365 - 2}{365} × \cdots × \frac{365 - n + 1}{365} = \prod_{i=0}^{n-1}\frac{365-i}{365} P(A)=365365×365365−1×365365−2×⋯×365365−n+1=i=0∏n−1365365−i 该式的意义为,
1 1 1 个人时生日不同的概率显然为 100 100 100%,而第 2 2 2 个人的开始,为了与前面的人生日不同,只能从前面的人生日的其他日期任选一个,于是第 2 2 2 个人有 364 365 \frac{364}{365} 365364 的概率与前面所有人的生日不同,第 3 3 3 人为 363 365 \frac{363}{365} 365363, ⋯ \cdots ⋯,最终这个事件的总概率为它们的乘积。
借助计算机,我们可以得知,当 n = 23 n = 23 n=23 时, P ( A ˉ ) = 1 − P ( A ) ≃ 0.5 P(\bar A) = 1 - P(A) \simeq 0.5 P(Aˉ)=1−P(A)≃0.5,当 n = 57 n = 57 n=57, P ( A ˉ ) ≃ 0.99 P(\bar A) \simeq 0.99 P(Aˉ)≃0.99。
生日悖论启示我们,某个长度为 n n n,分布在为 [ 1 , N ] [1,N] [1,N] 的随机数列 x 1 , x 2 , x 3 , ⋯ , x n x_1, x_2, x_3, \cdots , x_n x1,x2,x3,⋯,xn,它们中最少出现两个相等数字的期望长度不会很大(实际为 π N 2 \sqrt{\cfrac{\pi N}2} 2πN)。
设 N N N 的某个因子为 p p p,若我们不断的在 [ 1 , N ) [1,N) [1,N) 之间生成随机数 x i x_i xi,同时构造一个随机数列 { y i ∣ y i = x i m o d p } \{y_i \mid y_i = x_i \mod p\} {yi∣yi=ximodp},那么当存在一对 i i i、 j j j,使得 x i ≠ x j x_i \neq x_j xi=xj, y i = y j y_i = y_j yi=yj 同时成立,另 d = y i − y j d = y_i - y_j d=yi−yj,显然有 d ≡ 0 ( m o d p ) d \equiv 0\pmod p d≡0(modp), 1 < gcd ( d , N ) < N 1 < \gcd(d,N)
考虑最坏情况,即 N = p 2 N = p^2 N=p2, p p p 为一个质数( N N N 本身就是质数的情况可以先用 Miller-Rabin 素性测试特判一下),此时 p = N p = \sqrt N p=N, { y i } \{y_i\} {yi} 的期望长度为 π N 2 ≃ N 1 4 \sqrt{\cfrac{\pi \sqrt N}2} \simeq N^{\frac 14} 2πN≃N41。
光整合上述知识,想要快速的将一个较大整数 N N N 分解为算术基本定理形式也是不够的,光枚举 d d d 的复杂度就已经到了 O ( N ) O(\sqrt N) O(N),更别提对于每一次枚举还要做一次 gcd \gcd gcd。
所以 Pollard 选择一种特殊的伪随机数生成器, x i = x i − 1 2 + c ( m o d N ) x_i = x_{i-1}^2 + c \pmod N xi=xi−12+c(modN),其中 c c c 为某个固定常数, x 1 x_1 x1 随机取得。
例如当 N = 41 , x 1 = 31 , c = 5 N = 41,x_1 = 31,c=5 N=41,x1=31,c=5 时,生成的随机数列为: 31 , 23 , 1 , 6 , 0 , 5 , 30 ˙ , 3 , 14 , 37 , 21 , 36 ˙ , 30 , ⋯ 31,23, 1, 6, 0, 5, \dot{30}, 3, 14, 37, 21, \dot{36}, 30,\cdots 31,23,1,6,0,5,30˙,3,14,37,21,36˙,30,⋯ 将数列按下图所示方式排列,会发现性质酷似一个 ρ \rho ρ,算法也因此得名。
(图片摘自 wiki)
主流的优化方式有 Floyd 判环法,倍增法,这里仅对 判环法 做出阐述和实现。
若 { x i } \{x_i\} {xi} 上存在一对 i i i、 j j j 使得 x i − x j ≡ 0 ( m o d p ) x_i - x_j \equiv 0\pmod p xi−xj≡0(modp),则有
x i − x j ≡ x i − 1 2 − x j − 1 2 ≡ ( x i − 1 + x j − 1 ) ( x i − 1 − x j − 1 ) ≡ 0 ( m o d p ) x_i - x_j \equiv x_{i-1}^2 - x_{j-1}^2\equiv (x_{i-1} + x_{j-1})(x_{i-1} - x_{j-1})\equiv 0\pmod p xi−xj≡xi−12−xj−12≡(xi−1+xj−1)(xi−1−xj−1)≡0(modp),
该式表明了,对于所有 k , g k,g k,g, k − g = i − j k-g=i-j k−g=i−j,都有 x k − x g ≡ 0 ( m o d p ) x_k - x_g\equiv 0\pmod p xk−xg≡0(modp),因此使用 Floyd 判环法,依次枚举环上距离为 1 ∼ n 1 \sim n 1∼n 的随机数对,期望枚举次数为 n = N 1 4 n=N^\frac 14 n=N41,
整个算法的期望复杂度为 O ( N 1 4 log N ) O(N^\frac 14\log N) O(N41logN)。
int f(int x, int c) { return x * x + c; }
int gcd(int a, int b) { return b ? gcd(b, a % b) : a; }
int pollard_rho(int N, int c) {
int xi = rand() % (N - 1) + 1, xj = f(xi, c) % N;
while (xi != xj) {
int d = gcd(N, xi - xj);
if (d > 1) return d;
xj = f(f(xj, c), c) % N;
xi = f(xi, c) % N;
}
return N;
}
综合上述所有知识点,我们可以确定出一个算法,可以在较为优秀的复杂度内完成对一个大整数的分解。
具体地说,对于一个整数 N N N,我们先对其进行素性测试,若是则将其加入质因数集合,否则使用 Pollard’s Rho 找到他的一个非平凡因子 d d d,将 d d d 和 N / d N / d N/d 代回到第一步。
#include
#include
#include
long long multi(long long a, long long b, long long p) {
long long res = 0;
while (b) {
if (b & 1) res = (res + a) % p;
a = (a + a) % p;
b >>= 1;
}
return res;
}
long long qpow(long long a, long long b, long long p) {
long long res = 1;
while (b) {
if (b & 1) res = multi(res, a, p);
a = multi(a, a, p);
b >>= 1;
}
return res;
}
long long gcd(long long a, long long b) { return b ? gcd(b, a % b) : a; }
int test_time = 8;
bool miller_rabin(long long n) {
if (n < 3 || n % 2 == 0) return n == 2;
long long b = n - 1, k = 0, j;
while (b % 2 == 1) b /= 2, ++k;
for (int i = 0; i < test_time; ++i) {
long long a = qpow(rand() % (n - 2) + 2, b, n);
if (a == 1) continue;
for (j = 0; j < k; ++j) {
if (a != n - 1) break;
a = multi(a, a, n);
}
if (j == k) return 0;
}
return 1;
}
long long f(long long x, long long c, long long p) { return (multi(x, x, p) + c) % p; }
long long pollard_rho(long long N, long long c) {
long long xi = rand() % (N - 1) + 1, xj = f(xi, c, N);
while (xi != xj) {
long long d = gcd(xi - xj, N);
if (d > 1) return d;
xj = f(f(xj, c, N), c, N);
xi = f(xi, c, N);
}
return N;
}
void factor(long long N, std::map<long long, int> &factors) {
if (miller_rabin(N)) ++factors[N];
else {
long long c = rand() % (N - 1) + 1;
long long d = N;
while (d >= N)
d = pollard_rho(N, c--);
factor(N / d, factors);
factor(d, factors);
}
}
int main(){
long long N;
while (~scanf("%lld", &N)) {
std::map<long long, int> factors;
printf("%lld=", N);
factor(N, factors);
for (std::map<long long, int>::iterator it = factors.begin(); it != factors.end();) {
printf("%d^%d", it->first, it->second);
if (++it != factors.end()) printf("*");
}
printf("\n");
}
}