前言
该算法用于对大数进行质因子分解。 它的核心思想是:对于一个数N,若N不为质数,设N=c*d(c、d均大于1),我们找到一个c,递归分解c和d。
生日悖论
一个班有k个同学,至少有两名学生的生日是同一天的概率P是多少? 正难则反 ,考虑容斥。P=1-任意两名学生的生日都不同的概率。
k
=
1
,
P
=
0
k=1,P=0
k = 1 , P = 0
k
=
2
,
P
=
1
−
1
∗
364
365
k=2,P=1-1*\frac{364}{365}
k = 2 , P = 1 − 1 ∗ 3 6 5 3 6 4
k
=
3
,
P
=
1
−
1
∗
364
365
∗
363
365
k=3,P=1-1*\frac{364}{365}*\frac{363}{365}
k = 3 , P = 1 − 1 ∗ 3 6 5 3 6 4 ∗ 3 6 5 3 6 3
…
…
……
… …
k
=
23
,
P
=
0.507
k=23,P=0.507
k = 2 3 , P = 0 . 5 0 7 当k>55时,P就很接近于1了。 当k=100时,P=0.999999692751072。 其实,如果说一年有N天,那么只要
k
≥
N
k≥\sqrt N
k ≥ N
,他们中存在相同生日的概率就会大于50%。(详见《算导》黑书)
这有什么关联吗
估计大家刚看到生日悖论会和我产生同样的疑问:这有什么关联吗? 实际上,关联很大。 我们从[2,N]范围内随机k个数,刚好刷出c或d的概率很小,但如果它们两两作差呢? 拿c来说,我们选出k个数,如果它们有两个数的差的绝对值=c,那就perfect了!
用上面的例子来说,一个班有k个同学,我们想要知道至少有两名同学的生日恰好相差c天 的概率是多少。 而且,这次的形势比生日悖论要乐观许多。毕竟恰好相差c天 ,早c天、晚c天都可以;而且这里的c代指任意一个N的非平凡因子,若N不为质数,那少说也有两个c满足条件。 根据生日悖论的分析,当K取到
N
\sqrt N
N
时,概率就>50%了。
虽然很有道理,但是我们可以更稳一点。 在这k个数中,如果有两个数a、b,满足gcd(|a-b|,N)>1,那么我们就可以直接选这个gcd了。 这么一来,概率就翻了好多倍了。 因为N=c*d,c和d都很稀有,但是c和d的倍数便是一波大军。
流水线
如果我们要直接取k个数,那么要取到
N
1
4
N^{\frac 14}
N 4 1 个数,那样很耗空间。 可以不断生成伪随机数,然后像流水线一样判断。 设随机函数
f
(
x
)
=
(
x
2
+
c
)
m
o
d
N
f(x)=(x^2+c)\ mod\ N
f ( x ) = ( x 2 + c ) m o d N ,其中c为一个定值,可以随机一个。
int find_factor ( int N) {
a = rand ( ) ;
for ( int i= 1 ; i <= 1000000 ; i++ ) {
b = f ( a) ;
p = GCD ( abs ( b - a ) , N) ;
if ( p > 1 ) return p;
a = b;
}
return 0 ;
}
判断循环
Floyd判圈
发现上面的伪随机函数会循环。我们应如何判环呢? 可以傻逼逼地开个布尔数组,记录一下出现过的数。但是f(x)的取值范围可是0~N-1的;而当N超过
1
0
8
10^8
1 0 8 的时候,你就gg了。 刘汝佳先生说:想象一下,假设有两个小孩子在一个“可以无限向前跑”的跑道上赛跑,同时出发。但其中一个小孩的速度是另一个的两倍。如果跑道是直的,跑得快的小孩永远在前面;但如果跑道有环,则跑得快的小孩将“追上”跑得慢的小孩。 于是我们yy出了传说中的“Floyd判圈”算法。
int find_factor ( int N) {
a = rand ( ) ;
b = a;
do {
a = f ( a) ;
b = f ( f ( b) ) ;
p = GCD ( abs ( b - a ) , N) ;
if ( p > 1 ) return p;
} while ( b != a ) ;
return 0 ;
}
注:据维基百科说,即使n是合数,该算法也可能无法找到非平凡因子。在这种情况下,可以使用除2或不同的起始值再次尝试该方法。
另一种方法——Brent判圈
还有一种做法,是由提出的一种判圈法,又叫Brent判圈。 这种做法就是记录一下当前跑到第几个数了,如果跑到2的某个次幂,就将b=a,每次也是求GCD( abs( b - a ) , N),若某次新随机出的a=b则return。
ll get ( ll k)
{
ll a= rand ( ) % ( k+ 1 ) , b= a, c= rand ( ) % k, r, i= 1 , n= 2 ;
while ( 1 )
{
i++ ;
a= f ( a) ;
if ( a== b) return k;
r= gcd ( abs ( a- b) , k) ;
if ( 1 < r&& r< k) return r;
if ( i== n) b= a, n<<= 1 ;
}
}
Brent声称,平均而言,他的循环搜索算法比Floyd的运行速度快36%左右,并且它将Pollard rho算法加速了大约24%。
失败了怎么破
如果算法失败了,我们只需重新弄一个种子,再来一发即可。 不过请放心,大多数时候你并不会失败。
与有机结合♂♂
我们可以发现pollard rho直到现在都还没有与Miller-Rabin有任何联系,但马上就不是了。 对于pollard rho,它可以在Θ(sqrt§)的时间复杂度内找到N的一个小因子p,这一点我们曾论证过。可见,如果N的因子很多、因子值很小的整数N来说,效率是很优异的。 但是,如果反过来呢?如果说N是大整数,恰好因子很少、因子值很大?
譬如,如果N是一个超大的质数,会发生什么结果? 我们会源源不断地生产随机数,但是每次我们都发现GCD( abs( b - a ) , N) =1,于是每次都失败;然后重设种子,继续奋战……直到最后,你都很难判断这个N是否真的不可约。 但是,一旦拥有Miller_Rabin,一切便都已解决。 我们在想要分解N的时候,预先对其进行素性判定,若其通过,则视其为素数。虽然这么做会降低我们的正确率,但却能显著提高我们的速率。
Code
ll ti ( ll x, ll y, ll m)
{
ld d= 1 ;
d= d* x/ m* y;
return ( ( x* y- ( ( ll) d) * m) % m+ m) % m;
}
ll gcd ( ll x, ll y)
{
return y? gcd ( y, x% y) : x;
}
ll get ( ll k)
{
ll a= rand ( ) % ( k+ 1 ) , b= a, c= rand ( ) % k, r, i= 1 , n= 2 ;
while ( 1 )
{
i++ ;
a= ( ti ( a, a, k) + c) % k;
if ( a== b) return k;
r= gcd ( abs ( a- b) , k) ;
if ( 1 < r&& r< k) return r;
if ( i== n) b= a, n<<= 1 ;
}
}
void pollard_rho ( ll k)
{
if ( k== 1 ) return ;
if ( miller_rabin ( k) ) { pf[ ++ pf[ 0 ] ] = k; return ; }
ll p= k;
while ( p== k) p= get ( k) ;
pollard_rho ( k/ p) , pollard_rho ( p) ;
}
例题