一.素数定义与判定.
素数:又称质数,一个大于 1 1 1的正整数 n n n定义为素数,当且仅当不存在一个数字 i ∈ [ 2 , n ) i\in [2,n) i∈[2,n)满足 i ∣ n i|n i∣n.
之后我们将素数集记为 P P P.
合数:一个大于 1 1 1的正整数定义为合数,当且仅当它不是个素数.
根据素数的定义,我们可以很容易的写出一个判定素数的代码:
bool Check_prime(int n){
if (n<=1) return 0;
for (int i=2;i<n;++i)
if (n%i==0) return 0;
return 1;
}
时间复杂度 O ( n ) O(n) O(n).
显然每次都枚举 n n n个数来判定有些浪费,所以考虑优化这个算法.
容易注意到一个性质,若两个小于 n n n的数 x , y x,y x,y相乘要等于 n n n,则 x , y x,y x,y中必然有一个数不超过 n \sqrt{n} n,所以我们只需要枚举到 n \sqrt{n} n就可以了.
时间复杂度 O ( n ) O(\sqrt{n}) O(n).
代码如下:
bool Check_prime(int n){
if (n<=1) return 0;
for (int i=2;i*i<=n;++i)
if (n%i==0) return 0;
return 1;
}
二.素数个数相关.
素数个数函数:定义素数个数
π
(
n
)
\pi(n)
π(n)为:
π
(
n
)
=
∑
i
=
1
n
[
i
∈
P
]
\pi(n)=\sum_{i=1}^{n}[i\in P]
π(n)=i=1∑n[i∈P]
素数定理: π ( n ) ≈ n ln n \pi(n)\approx \frac{n}{\ln n} π(n)≈lnnn.
素数定理的推论1:第 n n n个素数的大小约为 n ln n n\ln n nlnn.
素数定理的推论2:设第
i
i
i个素数为
p
i
p_i
pi,有:
O
(
∑
p
i
≤
n
1
p
i
)
=
O
(
log
log
n
)
.
O\left(\sum_{p_i\leq n}\frac{1}{p_i}\right)=O(\log \log n).
O(pi≤n∑pi1)=O(loglogn).
证明:
O
(
∑
p
i
≤
n
1
p
i
)
=
O
(
∑
i
≤
π
(
n
)
1
i
log
i
)
=
O
(
∫
1
π
(
n
)
1
x
log
x
d
x
)
=
O
(
log
log
π
(
n
)
)
=
O
(
log
log
n
)
O\left(\sum_{p_i\leq n}\frac{1}{p_i}\right)=O\left(\sum_{i\leq \pi(n)}\frac{1}{i\log i}\right)\\ =O\left(\int_{1}^{\pi(n)}\frac{1}{x\log x}\mathrm{d} x\right)\\ =O(\log \log \pi(n))\\ =O(\log \log n)
O(pi≤n∑pi1)=O⎝⎛i≤π(n)∑ilogi1⎠⎞=O(∫1π(n)xlogx1dx)=O(loglogπ(n))=O(loglogn)
证毕.
三.素数筛法的引入与埃式筛法.
考虑如何求出 [ 1 , n ] [1,n] [1,n]中的所有素数.
根据上面的判定,我们可以很容易写出一个 O ( n n ) O(n\sqrt{n}) O(nn)的算法:
bool Check_prime(int n){
if (n<=1) return 0;
for (int i=2;i*i<=n;++i)
if (n%i==0) return 0;
return 1;
}
int n,pr[N+9],cp;
void Sieve(){
for (int i=2;i<=n;++i)
if (Check_prime(i)) pr[++cp]=i;
}
但是这个复杂度好像并不能满足我们的需求,有没有更快的算法呢?
考虑不要一个一个枚举后判定,而是开一个标记数组 b b b.对于每个数 i i i,我们把 2 i , 3 i , 4 i , ⋯ 2i,3i,4i,\cdots 2i,3i,4i,⋯都标记为 0 0 0,最后把所有标记为 1 1 1的数取出就是素数.
容易发现,所有非素数 i i i的倍数肯定会被 i i i的某个因数筛掉,所以我们就不用枚举合数的倍数了,直接枚举所有素数的倍数筛就好了.
时间复杂度 O ( n log log n ) O(n\log \log n) O(nloglogn).
代码如下:
int n,b[N+9],pr[N+9],cp;
void Sieve(){
for (int i=2;i<=n;++i){
if (!b[i]) pr[++cp]=i;
for (int j=1;j<=cp&&i*pr[j]<=n;++j) b[i*pr[j]]=1;
}
}
以上筛法被称为埃式筛法.
埃式筛法还可以在
O
(
(
r
+
r
−
l
)
log
log
r
)
O((\sqrt{r}+r-l)\log \log r)
O((r+r−l)loglogr)的复杂度内筛出区间
[
l
,
r
]
[l,r]
[l,r]内的素数,具体参见POJ2689 Prime Distance.
四.线性筛法.
事实上,还有一种比埃式筛更快的线性筛法,学名欧拉筛,它的时间复杂度是稳定 O ( n ) O(n) O(n)的.
它的具体思路是,在埃式筛法的基础上,让每一个数都只被它的最小素因子筛掉.
具体来说,我们枚举 j j j并筛掉 i p j ip_j ipj的时候,若 p j ∣ i p_j|i pj∣i,那么 p j + 1 p_{j+1} pj+1必然不是 i p j + 1 ip_{j+1} ipj+1的最小素因子,此时就可以直接退出了.
代码如下:
int n,b[N+9],pr[N+9],cp;
void Sieve(){
for (int i=2;i<=n;++i){
if (!b[i]) pr[++cp]=i;
for (int j=1;j<=cp&&i*pr[j]<=n;++j){
b[i*pr[j]]=1;
if (i%pr[j]==0) break;
}
}
}
五.素因子与唯一分解定理.
素因子:定义一个数 a a a是 n n n的素因子,当且仅当 a ∈ P a\in P a∈P且 a ∣ n a|n a∣n.
唯一分解定理:对于任意一个正整数
n
n
n,必然可以被唯一分解为:
n
=
∏
i
=
1
k
p
i
c
i
n=\prod_{i=1}^{k}p_i^{c_i}
n=i=1∏kpici
其中 p i ∈ P p_i\in P pi∈P.
结论1:一个数
n
n
n的素因子个数不超过
log
n
\log n
logn个,即:
∑
i
=
1
k
c
i
≤
log
n
\sum_{i=1}^{k}c_i\leq \log n
i=1∑kci≤logn
结论2:一个数
n
n
n本质不同的素因子个数不超过
log
log
n
\log \log n
loglogn个,即:
k
≤
log
log
n
k\leq \log \log n
k≤loglogn
现在我们又有了一个新问题,如何分解出 n n n的所有 p i p_i pi与对应的 c i c_i ci.
首先一个显然的性质,必然只有最多一个素因子大于 n \sqrt{n} n,所以我们可以只从 2 2 2枚举到 n \sqrt{n} n.
此时我们存一个暂存变量 n o w now now初始为 n n n,每当我们枚举到一个数 i ∣ n o w i|now i∣now,就让 n o w now now不停除 i i i到 n o w now now没有 i i i这个因数,此时把 i i i放入 p p p中并把除的此时作为对应的 c c c即可.
时间复杂度 O ( n ) O(\sqrt{n}) O(n).
代码如下:
int d[2][C+9],cd;
void Get_d(int n){
int now=n;
for (int i=2;i*i<=n;++i)
if (now%i==0)
for (d[0][++cd]=i;now%i==0;now/=i) ++d[1][cd];
if (now>1) d[0][++cd]=now,d[1][cd]=1;
}
六.求1到n的唯一分解.
现在我们有一个新的问题,如何求 [ 1 , n ] [1,n] [1,n]中所有数的唯一分解.
我们当然可以用上面单个数的分解方法做到 O ( n n ) O(n\sqrt{n}) O(nn)分解,但是还可以用筛法做到 O ( n log n ) O(n\log n) O(nlogn).
具体来说是这样的,由于在线性筛的时候每次都是用最小的素因子筛掉某个数,所以我们可以顺便记录每个数的最小素因子.有了每个数的最小素因子之后,我们就可以用一个递归或者循环来求解了.
时间复杂度 O ( n log n ) O(n\log n) O(nlogn).
代码如下:
int n,b[N+9],pr[N+9],cp;
int ml[N+9];
void Sieve(){
for (int i=2;i<=n;++i){
if (!b[i]) pr[++cp]=i,ml[i]=i;
for (int j=1;j<=cp&&i*pr[j]<=n;++j){
b[i*pr[j]]=1;
ml[i*pr[j]]=pr[j];
if (i%pr[j]==0) break;
}
}
}
int d[2][N+9][C+9],cd[N+9];
void Get_d(){
for (int i=2;i<=n;++i){
int now=i;
for (;now>1;now/=ml[now])
if (ml[now]==d[0][i][cd[i]]) ++d[1][i][cd[i]];
else d[0][i][++cd[i]]=ml[now],d[1][i][cd[i]]=1;
}
}
//主程序中
Sieve();
Get_d();
七.阶乘的素因子个数统计.
我们有方法在 O ( log n ) O(\log n) O(logn)的时间复杂度内统计 n ! n! n!的某个素因子 p p p的出现次数.
首先,我们先把 [ 1 , n ] [1,n] [1,n]中有因数 p p p的数的个数,显然为 ⌊ n p ⌋ \left\lfloor\frac{n}{p}\right\rfloor ⌊pn⌋.
然后,我们发现所有 p p p的都有了 1 1 1的贡献,但是 p 2 p^2 p2的倍数会有 2 2 2的贡献,所以 p 2 p^2 p2的倍数的贡献应该再加上 1 1 1,也就是 ⌊ n p 2 ⌋ \left\lfloor\frac{n}{p^2}\right\rfloor ⌊p2n⌋.
以此类推,我们可以得到答案为:
a
n
s
=
∑
i
=
1
+
∞
⌊
n
p
i
⌋
ans=\sum_{i=1}^{+\infty}\left\lfloor\frac{n}{p^{i}}\right\rfloor
ans=i=1∑+∞⌊pin⌋
显然计算这个式子可以做到
O
(
log
n
)
O(\log n)
O(logn).
八.Miller-Rabbin素性测试.
如何用快于 O ( n ) O(\sqrt{n}) O(n)的时间复杂度确定 n n n是否是一个素数呢?
一个不幸的事实,保证正确性的做法中,最快的是 O ( n ) O(\sqrt{n}) O(n)的朴素和一个被称为AKS素性测试的上界至少为 O ( log 6 n ) O(\log^{6}n) O(log6n)的算法.
但是我们为什么一定要让这个算法一定正确?有没有什么虽然不能确保正确但正确率很高的做法呢?
考虑费马小定理的逆命题,即:
a
n
−
1
≡
1
(
m
o
d
n
)
⇒
a
∈
P
a^{n-1}\equiv 1\,\,(mod\,\,n)\Rightarrow a\in P
an−1≡1(modn)⇒a∈P
这个命题虽然不是真命题,但它的出错概率非常小,通常随机个位数个 a a a就可以保证很高的正确率.
同时有一个二次探测定理:对于一个素数 n n n,若有 x x x满足 x 2 ≡ 1 ( m o d n ) x^2\equiv 1\,\,(mod\,\,n) x2≡1(modn),则必然有 x = 1 x=1 x=1或 x = n − 1 x=n-1 x=n−1.
证明:
首先
n
=
2
n=2
n=2时定理显然成立.
x
2
≡
1
(
m
o
d
n
)
⇔
n
∣
(
x
2
−
1
)
⇔
n
∣
(
x
+
1
)
(
x
−
1
)
⇔
n
∣
(
x
+
1
)
∨
n
∣
(
x
−
1
)
x^2\equiv 1\,\,(mod\,\,n)\Leftrightarrow n|(x^2-1)\Leftrightarrow n|(x+1)(x-1)\Leftrightarrow n|(x+1)\vee n|(x-1)
x2≡1(modn)⇔n∣(x2−1)⇔n∣(x+1)(x−1)⇔n∣(x+1)∨n∣(x−1)
由于
n
∈
P
n\in P
n∈P,所以只能取
x
=
1
x=1
x=1或
x
=
n
−
1
x=n-1
x=n−1.
证毕.
这个时候我们考虑在判定一个数 n n n是否为素数的时候,先把 2 2 2和 n n n为偶数的情况判掉.之后把 n − 1 n-1 n−1中所有 2 2 2提出来后的值记为 t t t,并把 2 2 2的个数记为 c n t cnt cnt,即 n − 1 = 2 c n t t n-1=2^{cnt}t n−1=2cntt.
然后对于每一个随机值 a a a,若 a t ≡ 1 ( m o d n ) a^{t}\equiv 1\,\,(mod\,\,n) at≡1(modn)或 a t ≡ n − 1 ( m o d n ) a^{t}\equiv n-1\,\,(mod\,\,n) at≡n−1(modn),则判定这是个素数.
否则,我们把 c n t cnt cnt个 2 2 2乘回去,记当前值为 n o w now now,若 n o w 2 ≡ 1 ( m o d n ) now^2\equiv 1\,\,(mod\,\,n) now2≡1(modn)且 n o w ≢ 1 ( m o d n ) now\not\equiv1\,\,(mod\,\,n) now≡1(modn)且 n o w ≢ n − 1 ( m o d n ) now\not\equiv n-1\,\,(mod\,\,n) now≡n−1(modn),说明不满足二次探测定理,判定这不是个素数.否则把 n o w now now平方继续下一次操作.
注意通常要这个算法来处理的时候, n 2 n^2 n2的大小已经可以爆long long了,所以需要用到快速乘.
时间复杂度 O ( k log n ) O(k\log n) O(klogn),其中 k k k表示取 a a a的次数.
代码如下:
const int pr[9]={2,3,5,7,11,13,17,19,23};
LL gcd(LL a,LL b){return b?gcd(b,a%b):a;}
LL add(LL a,LL b,LL p){return a+b>=p?a+b-p:a+b;}
LL sub(LL a,LL b,LL p){return a-b<0?a-b+p:a-b;}
LL mul(LL a,LL b,LL p){
LL t=(long double)a/p*b+1e-8,res=a*b-t*p;
return (res<0?res+p:res)%p;
}
void sadd(LL &a,LL b,LL p){a=add(a,b,p);}
void ssub(LL &a,LL b,LL p){a=sub(a,b,p);}
void smul(LL &a,LL b,LL p){a=mul(a,b,p);}
LL Power(LL a,LL k,LL p){LL res=1;for (;k;k>>=1,smul(a,a,p)) if (k&1) smul(res,a,p);return res;}
bool Miller_rabbin(LL n){
if (n==2) return 1;
if (n&1^1||n<=1) return 0;
LL t=n-1;
int cnt=0;
for (;t&1^1;t>>=1) ++cnt;
for (int i=0;i<9;++i){
if (n==(LL)pr[i]) return 1;
LL a=Power(pr[i]%n,t,n);
if (a==1||a==n-1) continue;
for (int j=1;j<=cnt;++j){
LL t=mul(a,a,n);
if (a^1&&a^n-1&&t==1) return 0;
a=t;
}
if (a^1) return 0;
}
return 1;
}
九.Pollard-rho算法.
Pollard-rho算法是一种用于寻找一个数 n n n的不是 1 1 1和 n n n的因数的算法.
Pollard-rho算法的基本思想是,生成一个随机数列 f i f_i fi,然后把随机数列中相邻两项做差并取绝对值,用这些值去和 n n n取gcd,只要取出的gcd大于 1 1 1且小于 n n n就可以分解出这个因数.
有一个较为优秀的伪随机函数 f i = ( f i − 1 2 + c ) m o d n f_i=(f_{i-1}^2+c)\,\,mod\,\,n fi=(fi−12+c)modn,其中 c c c是一个随机的常数,通过这个伪随机函数有较大概率得到一个优秀的因数.
同时,由于数列是伪随机数列,所以还是会不可避免出现循环.此时我们有一个Floyd判环法,从起点出发,记录两个值 f i , f 2 i f_i,f_{2i} fi,f2i,一个用一倍速跑,一个用两倍速跑,若它们相同说明出现循环,随即跳出重新选择一个随机的 c c c.
不过具体实现的时候,我们可以不用真的记录两个值跑,而是在跑了 2 i 2^{i} 2i的距离后记录一下当前值 t t t,直到跑到 2 i + 1 2^{i+1} 2i+1前都与 t t t比较是否出现循环.
同时通过Pollard-rho算法配合分治得到一个数 n n n的所有素因子的时间复杂度是期望 O ( n 1 4 log n ) O(n^{\frac{1}{4}}\log n) O(n41logn)的,不过是在随机数列是真随机数列的时候.
若随机数列为 f i = ( f i − 1 2 + c ) m o d n f_i=(f_{i-1}^2+c)\,\,mod\,\,n fi=(fi−12+c)modn,则这样实现的Pollard-rho算法复杂度还是未知的,不过一般情况下我们仍然将其看为 O ( n 1 4 log n ) O(n^{\frac{1}{4}}\log n) O(n41logn).
代码如下:
const int pr[9]={2,3,5,7,11,13,17,19,23};
LL gcd(LL a,LL b){return b?gcd(b,a%b):a;}
LL add(LL a,LL b,LL p){return a+b>=p?a+b-p:a+b;}
LL sub(LL a,LL b,LL p){return a-b<0?a-b+p:a-b;}
LL mul(LL a,LL b,LL p){
LL t=(long double)a/p*b+1e-8,res=a*b-t*p;
return (res<0?res+p:res)%p;
}
void sadd(LL &a,LL b,LL p){a=add(a,b,p);}
void ssub(LL &a,LL b,LL p){a=sub(a,b,p);}
void smul(LL &a,LL b,LL p){a=mul(a,b,p);}
LL Power(LL a,LL k,LL p){LL res=1;for (;k;k>>=1,smul(a,a,p)) if (k&1) smul(res,a,p);return res;}
bool Miller_rabbin(LL n){
if (n==2) return 1;
if (n&1^1||n<=1) return 0;
LL t=n-1;
int cnt=0;
for (;t&1^1;t>>=1) ++cnt;
for (int i=0;i<9;++i){
if (n==(LL)pr[i]) return 1;
LL a=Power(pr[i]%n,t,n);
if (a==1||a==n-1) continue;
for (int j=1;j<=cnt;++j){
LL t=mul(a,a,n);
if (a^1&&a^n-1&&t==1) return 0;
a=t;
}
if (a^1) return 0;
}
return 1;
}
LL Pollard_rho(LL n){
if (Miller_rabbin(n)) return n;
for (;2333;){
LL x=rand()%(n-1)+1,y=x;
int c=rand()%(n-1)+1,len=2;
for (int i=0;2333;){
x=add(mul(x,x,n),c,n);
LL g=gcd(abs(x-y),n);
if (g>1&&g<n) return g;
if (x==y) break;
if (++i==len) y=x,len<<=1;
}
}
}
set<LL>s;
void Divide_s(LL n){
if (n==1||Miller_rabbin(n)) {if (n^1) s.insert(n);return;}
LL t=Pollard_rho(n);
for (;t==n;t=Pollard_rho(n));
for (;n%t==0;n/=t);
Divide_s(t);
Divide_s(n);
}