分解因数算法
给定一个正整数N,求其所有质因数
朴素算法
最贴近人的想法,就是从2开始一直到 N \sqrt{N} N,用N取除以这些数,只要是能够整除就是因数,然后判定该因数是否是素因数
for(int i=2;i<=sqrt(N);i++){
if(N%i==0){
if(isPrime(i)){
//i是N的素因数
}
}
}
唯一分解定理优化
任何正整数N都可以分解为若干个素数的乘积
N
=
p
1
k
1
p
2
k
2
.
.
.
p
n
k
n
N=p_1^{k_1}p_2^{k_2}...p_n^{k_n}
N=p1k1p2k2...pnkn
比如
48
=
2
4
×
3
1
48=2^4\times 3^1
48=24×31
对于刚才纯朴素的做法,我们在找到 2 2 2是48的素因数之后,如果不加处理后来又会发现4是48的因数但不是素因数,
可以将 2 k 2^k 2k从N的所有因数中去掉
int temp=N;
for(int i=2;i<=sqrt(temp);++i){
if(temp%i==0){
//i是temp的素因数,之所以没有判断素性就直接说是素因数,是因为有下面这个while保证
while(temp%i==0){//实际上相当于筛子,筛去了i^k因子
temp/=i;//由于temp%i=0因此i整除temp,此举将temp中所有i因数即其幂去掉
}
}
}
if(temp!=1){
//最后剩下的temp也是N的素因数
}
只判断 t e m p temp%i==0 temp貌似只能说明 i i i是 t e m p temp temp的因数,为什么还能将得到更严格结论" i i i是 t e m p temp temp的素因数"呢?
假设 i i i是 t e m p temp temp的合因数
根据唯一分解定理, i = p 1 p 2 . . . p n , p i 为 素 数 i=p_1p_2...p_n,p_i为素数 i=p1p2...pn,pi为素数
显然对于一个合数,i至少有两个素因数,不妨设为 1 < p 1 , p 2 < i 1<p_1,p_2<i 1<p1,p2<i
则 p 1 ∣ i , i ∣ t e m p ⇒ p 1 ∣ t e m p p_1|i,i|temp\Rightarrow p_1|temp p1∣i,i∣temp⇒p1∣temp
那么 p 1 p_1 p1是 t e m p temp temp的比i小的素因数
现在将时光回溯到 i = p 1 i=p_1 i=p1时,我们的while循环做了一件事:
while(temp%p1==0){ temp/=p1; }
此举保证了出while循环的时候,temp不再含有 p 1 p1 p1因子,也就是 p 1 ∤ t e m p p1 \nmid temp p1∤temp
现在时光前进到 i = t e m p i=temp i=temp时,我们刚刚得到结论 p 1 ∣ t e m p p_1| temp p1∣temp
于是产生了矛盾
因此假设不成立,即 i i i是 t e m p temp temp的素因数
Pollard Rho算法
ρ \rho ρ
算法思想
朴素算法是从 [ 2 , N ] [2,\sqrt{N}] [2,N]里面遍历所有数找N的因数
而现在我们不轮着找,而是挑着找,就类似于抽样检测.
用随便挑的两个数的差去找,啥意思呢?
∀ x 1 , x 2 ∈ [ 2 , N ] \forall x_1,x_2\in [2,\sqrt{N}] ∀x1,x2∈[2,N],我们求 ∣ x 2 − x 1 ∣ |x_2-x_1| ∣x2−x1∣,然后用这个数去试是否是 N N N的因数
为啥不直接选一个 ∀ x ∈ [ 2 , N ] \forall x\in[2,\sqrt{N}] ∀x∈[2,N],然后用这个x去比划N,而是选两个x的差去比划呢?
0.生日悖论
这个只能用概率解释了,生日悖论问题
1.考虑从 [ 1 , 1000 ] [1,1000] [1,1000]上选k个数,求任意两个数的差不等于x的概率
2.考虑一伙子k个人,任意两个人生日不同的概率(生日悖论)
上面那个我不会求,下面这个我会
假设一年有n天,求k个人生日都不相同的概率
第一个人的生日可以在n天中的任意一天,有n种情况
第二个人的生日不能和第一个人相同,有n-1种情况
第三个人的生日不能和前两个人相同,有n-2种情况
…
第k个人的生日不能和前k-1个人相同,有n-(k-1)种情况
因此k个人生日都不相同的概率为
P = n ( n − 1 ) ( n − 2 ) . . . ( n − ( k − 1 ) ) n k = n n × n − 1 n × n − 2 n × . . . × n − k + 1 n = 1 × ( 1 − 1 n ) × ( 1 − 2 n ) × . . . × ( 1 − k − 1 n ) P=\frac{n(n-1)(n-2)...(n-(k-1))}{n^k}\\ =\frac{n}{n}\times \frac{n-1}{n}\times \frac{n-2}{n}\times ...\times \frac{n-k+1}{n}\\ =1\times (1-\frac{1}{n})\times (1-\frac{2}{n})\times ...\times (1-\frac{k-1}{n}) P=nkn(n−1)(n−2)...(n−(k−1))=nn×nn−1×nn−2×...×nn−k+1=1×(1−n1)×(1−n2)×...×(1−nk−1)
这个数可不好求,但是可以放缩,用到的是高中就见过的老伙计了 e x ≥ x + 1 e^x\ge x+1 ex≥x+1
1 − i n ≤ e − i n 1-\frac{i}{n}\le e^{-\frac{i}{n}} 1−ni≤e−ni
因此
P = 1 × ( 1 − 1 n ) × ( 1 − 2 n ) × . . . × ( 1 − k − 1 n ) ≤ e − 1 n × e − 2 n × . . . × e − k − 1 n = e − 1 n ∑ i = 1 k − 1 i = e − k ( k − 1 ) 2 n P=1\times (1-\frac{1}{n})\times (1-\frac{2}{n})\times ...\times (1-\frac{k-1}{n})\\ \le e^{-\frac{1}{n}}\times e^{-\frac{2}{n}}\times ...\times e^{-\frac{k-1}{n}}\\ =e^{-\frac{1}{n}\sum_{i=1}^{k-1} i}\\ =e^{-\frac{k(k-1)}{2n}} P=1×(1−n1)×(1−n2)×...×(1−nk−1)≤e−n1×e−n2×...×e−nk−1=e−n1∑i=1k−1i=e−2nk(k−1)
即k个人生日都不同的概率小于等于 e − k ( k − 1 ) 2 n e^{-\frac{k(k-1)}{2n}} e−2nk(k−1)那么存在两个人生日相同的概率为 1 − e − k ( k − 1 ) 2 n 1-e^{-\frac{k(k-1)}{2n}} 1−e−2nk(k−1)
忙活半天求这个概率的缩放值有啥目的呢?
为了证明当人稍微多一点,存在两个人生日同一天的概率就会变大,并且是超出直觉的大
比如n=365,求存在两个人生日相同的概率达到50%,至少需要几个人
1 − e − k ( k − 1 ) 730 ≥ 1 2 1-e^{-\frac{k(k-1)}{730}}\ge \frac{1}{2}\\ 1−e−730k(k−1)≥21k ( k − 1 ) ≥ 730 ln 2 = 505.99744... ≈ 506 k(k-1)\ge 730\ln 2=505.99744...\approx 506 k(k−1)≥730ln2=505.99744...≈506
而 23 ∗ 22 = 506 23*22=506 23∗22=506
因此 k ≥ 23 k\ge 23 k≥23
即至少23个人就可以
想想这个事情,随便路上找一个人比较生日,相同的概率只有1/365
但是随便找22个人都问一遍,就会有一半的概率,其中至少有一个人和我同生日
放在这里,求因数,怎么操作呢?
我们要产生一系列 [ 2 , N ] [2,\sqrt{N}] [2,N]上的随机数,然后每次取一对做差检验这个差是不是N的因数
1.如何产生随机数
构造随机数函数
f
(
x
)
=
(
x
2
+
c
)
m
o
d
n
f(x)=(x^2+c)\mod n
f(x)=(x2+c)modn,作用是生成随机数序列,其中c是随便指定的一个数,然后随便选取一个正整数
x
0
x_0
x0作为起点
x
1
=
f
(
x
0
)
=
(
x
0
2
+
c
)
m
o
d
n
x
2
=
f
(
x
1
)
=
(
x
1
2
+
c
)
m
o
d
n
.
.
.
x
m
=
f
(
x
m
−
1
)
=
(
x
m
−
1
2
+
c
)
m
o
d
n
x_1=f(x_0)=(x_0^2+c)\mod n\\ x_2=f(x_1)=(x_1^2+c)\mod n\\ ...\\ x_m=f(x_{m-1})=(x_{m-1}^2+c)\mod n
x1=f(x0)=(x02+c)modnx2=f(x1)=(x12+c)modn...xm=f(xm−1)=(xm−12+c)modn
计算若干次后一定会陷入循环
为什么一定会陷入循环?
假设不会陷入循环,
则每一次计算都会得到一个与前面所有 x i x_i xi都不同的x值,但是 ∀ x i ∈ [ 0 , n − 1 ] \forall x_i\in[0,n-1] ∀xi∈[0,n−1],即 x i x_i xi顶多有n种取值,那么至多计算n次之后,由抽屉原理,一定会又两个 x x x值相同.
假设是 x j = f ( f ( f ( f ( . . . f ( x i ) ) ) ) ) x_j=f(f(f(f(...f(x_i))))) xj=f(f(f(f(...f(xi)))))即 x i x_i xi经过若干次迭代计算之后得到 x j x_j xj,并且 x i = x j x_i=x_j xi=xj
那么再计算 x j + 1 = f ( x j ) = f ( x i ) = x i + 1 x_{j+1}=f(x_j)=f(x_i)=x_{i+1} xj+1=f(xj)=f(xi)=xi+1
以此类推可以得到 x j + k = x i + k x_{j+k}=x_{i+k} xj+k=xi+k
显然陷入了循环
2.如何检查已经陷入了循环?
利用链表上快慢指针检查是否有环的思想
一个400米的圆形跑道,
两个傻子不知道跑道是⭕形状的,只知道沿着跑到一直跑
两个傻子同时同地起跑,快傻子每秒一米,慢傻子每秒半米,
相对速度差为每秒半米,那么经过800秒,相对路程差为 800 × 0.5 = 400 m 800\times 0.5=400m 800×0.5=400m,
快傻子正好比慢傻子快一圈,两人再次相遇表明"地球是圆的"即跑道是套圈的
在这里令 x i = f ( f ( x i − 1 ) ) , x j = f ( x j − 1 ) x_i=f(f(x_{i-1})),x_j=f(x_{j-1}) xi=f(f(xi−1)),xj=f(xj−1), x i x_i xi就是那个快傻子, x j x_j xj就是那个慢傻子,他俩就是那个海尔兄弟,舒克贝塔
并且两个傻子从并排起跑到再次相遇,他们的相对路程经理了一个从0到400的过程,遍历了 [ 0 , 400 ] [0,400] [0,400]间的所有值
那么在这里两个快慢指针的差也就遍历了 f ( x ) f(x) f(x)函数能够产生的,在圈上的,所有随机数的差
为啥说"在圈上的"
这里陷入循环不一定是从 x 0 x_0 x0就在循环中,
可能是中间某个状态才进入循环
就类似于一个 ρ \rho ρ,一开始的 x x x值是在 ρ \rho ρ的"腿"上的,后来的 x x x才进入 ρ \rho ρ的圈上
这个方法叫做"Floyd判环"
当两个快慢指针再次相遇的时候,说明所有 f ( x ) f(x) f(x)能够产生的圈上的随机数的差都不是 N N N的因数
那么只能说明选的随机数函数真的太逊了
然后对 f ( x ) = x 2 + c f(x)=x^2+c f(x)=x2+c进行修改比如修改一个初始值 x 0 ′ x_0' x0′或者修改 c c c或者直接修改函数表达式 f ( x ) = x 2 + x + c f(x)=x^2+x+c f(x)=x2+x+c等等
3.最大公因数优化
刚才我们只是检查 ∣ x i − x j ∣ |x_i-x_j| ∣xi−xj∣能不能整除 N N N,即检查 ∣ x i − x j ∣ |x_i-x_j| ∣xi−xj∣是不是 N N N的因数
但是由于 g c d ( ∣ x i − x j ∣ , N ) ∣ N a n d g c d ( ∣ x i − x j ∣ , N ) ∣ ∣ x i − x j ∣ gcd(|x_i-x_j|,N)|N\ and\ gcd(|x_i-x_j|,N)||x_i-x_j| gcd(∣xi−xj∣,N)∣N and gcd(∣xi−xj∣,N)∣∣xi−xj∣,那么只要 ∣ x i − x j ∣ |x_i-x_j| ∣xi−xj∣是N的因数,那么 g c d ( ∣ x i − x j ∣ , N ) gcd(|x_i-x_j|,N) gcd(∣xi−xj∣,N)也是N的因数
并且用最大公因数去和N比划的机会只会比只用 ∣ x i − x j ∣ |x_i-x_j| ∣xi−xj∣去直接比划N更多
对于 ∣ x i − x j ∣ |x_i-x_j| ∣xi−xj∣可以要求放松一点,只要是 ∣ x i − x j ∣ |x_i-x_j| ∣xi−xj∣的任何非1真因数能够整除 N N N就算是给N找到了一个因数
完整代码
#include <iostream>
#include <algorithm>
using namespace std;
long long gcd(const long long &a, const long long &b) {
if (b == 0)
return a;
return gcd(b, a % b);
}
long long f(const long long &x0, const long long &c, const long long &mod) {
return (x0 * x0 % mod + c) % mod;
}
long long Pollard_Rho(const long long &N) {
long long c = rand() % (N - 1) + 1; //c∈[1,N-1]
long long slow = f(0, c, N);
long long fast = f(f(0, c, N), c, N);
while (slow != fast) {
long long d = gcd(abs(slow - fast), N);
if (d > 1)
return d;//找到N的真因数d,返回
slow = f(slow, c, N);
fast = f(f(fast, c, N), c, N);
}
return N;//失败
}
long long N;
int time = 0;
int main() {
while (cin >> N) {
cout << ++time << ":";
cout << Pollard_Rho(N) << endl;
}
}
随便给几个数,这个程序的运行结果是怎样的呢?
8
1:8
8
2:4
8
3:8
8
4:4
8
5:8
8
6:8
9
7:9
9
8:9
9
9:9
9
10:3
可以发现,当N=8的时候,第一次失败了,第二次成功找到4,第三次又失败了,第四次又找到4,第五六次都失败了
当N=9,第九次失败,第十次找到三
如果真的失败了,多选上几个c去世或者改一下f函数去世.
如果试了很多次都失败了,就可以从概率上极大似然地认为N没有真因子了
BUUCTF-Alice和Bob
分解质因数 98554799767 98554799767 98554799767
还是运行刚才的程序
98554799767
1:98554799767
98554799767
2:101999
第二次就找到了真因子101999,我们只能确定他是 98554799767 98554799767 98554799767的因子,但是 101999 101999 101999是不是素数还没有检查
但是吧 101999 101999 101999用2,3,5,13,17这一些除一下都除不开,长成这样很像一个素数, 98554799767 / 101999 = 966233 98554799767/101999=966233 98554799767/101999=966233,直接提交101999和966233就通过了