高效因子分解
高效因子分解
本文主要包括了一些常见和不常见的高效因子分解算法,重点在于椭圆曲线法的讲解(虽然Dixon's 的篇幅更多一些貌似)
主要参考为 Wikipedia 的 Integer Factorization 页面
前言
质因数分解一直是一个密码学领域相当热门的话题,因为他直接关系着 RSA 等使用大素数相乘来加密的算法的安全性
相比于
-
求素数(筛法或者特殊的生成法)
-
素性测验(能做到
O((\log n))^{6+\epsilon} ) -
素数计数(求
[1,n] 的素数个数,可以使用Meissel-Lehmer在亚线性复杂度内求解)
因数分解一直是一个很困难的内容
现有的对于一般数字分解的最好算法是GNFS(General Number Field Sieve,一般数域筛)的复杂度为
能够分解上百位的合数
而数域筛涉及到大量的代数数论的内容,故笔者在此介绍一些更加简单易懂的算法包括 :
Dixon's ,CFRAC 和 Lanstra 椭圆曲线 算法 (你说Pollard's Rho ?建议左转Pollard's Rho)
大概只需要一些基本的数论知识(和一小点群论),同时给出了一个椭圆曲线法的Python Demo
基础的想法
试除
一个基础的想法是在
Fermat
而 Fermat 分解提供了一个新的思路
注意到若奇合数
变形一下得到
但是这样的复杂度为
比如我们考虑如果放宽要求
因为有
Pollard's Rho算法
该算法是 OI 中不常用的算法之一,但是是一个极好的高效因数分解的例子
Pollard's Rho算法的思想是在接近随机的数列
出现
而对于
于是只要
于是我们得到了
亚指数级
L记号
L 记号同样是衡量时间复杂度的,
其中
主要用来数论中算法的复杂度
Dixon's
Dixon's 的主要思想是:
选定一个大小为
然后通过奥妙重重的方法选取一系列
这样我们就得到了一个
那我们怎么才能寻找到这个奥妙重重的方法呢?
考虑
令
可以用高斯消元解出来向量
把对应的数字乘起来即可
而
大概分析下复杂度
对于确定的
而对于这个
取
连分数(CFRAC)
笔者并没有系统的学习过连分数相关的理论,故仅在此给出简单的介绍
连分数因数分解实际上在 Dixon's 上的一个优化
我们发现如果
那么用周期连分数表示
(抄的Wikipedia,如果有懂的大佬希望可以给出相关文献QAQ)
二次筛
二次筛法同样是对 Dixon's 的优化,可以做到
具体原理在学了,学会了也许会单独写一篇
椭圆曲线法
虽然椭圆曲线法同样是亚指数级别的,但是因为要给出比较详细的讲解故将其分离了出来
一些定义
椭圆曲线
椭圆曲线是域上亏格为1的光滑射影曲线
我们可以理解为形如
B-smooth
当我们说
B-powersmooth
同样的,B-powersmooth 指的是
椭圆曲线群
我们在椭圆曲线上的点
定义自己加自己是沿着自己的切线的交点
再定义无穷远点记作
Pollard's p-1
虽然 Pollard's p-1 算法甚至不是正确的(不一定能得到答案)
但是它仍然有着借鉴意义
Pollard's p-1 算法的思想是如果
那么
如果
我们选取一个 B-smooth 的数字
而
椭圆曲线法
我们将椭圆曲线限制在有理点上,可以发现群的结构仍然保持
我们可以考虑有限域
考虑椭圆曲线上的加法过程
令
则
如果我们在
流程
-
选定一条曲线
y^2=x^3+ax+b 与非平凡点P -
选定素数上界
B -
计算
(\prod\limits_{p_i<B}p_i^{\ln B/\ln p_i})P (\ln B/\ln p_i 是为了保证这个数 B-powersmooth) -
加法过程中寻找因子
算法原理
可以发现我们的基本思想是和 Pollard's p-1 相同,只不过把质数改成了在椭圆曲线群上的加法
所以它为什么比Pollard's p-1 要好呢?
Pollard's p-1 的本质在于在群
而这个群的阶
而根据 Hasse 定理,椭圆曲线群
这样群的阶就会有变化,避免了不 powersmooth 的问题
进一步我们考虑曲线
记他们的元素数量为
根据拉格朗日定理(
那么
于是和 Pollard's p-1 一样,如果我们找到了某个
复杂度
找到一个可分解的椭圆曲线群的概率为
若设
和 Dixon's 一样的分析能得到复杂度大致为
可以看到椭圆曲线法的复杂度主要取决于
所以椭圆曲线法可以快速去除一些小的素因子(大约
具体实现
操作中选取曲线上的非平凡点这个行为可以变为随机一个点然后生成一个曲线
对于点
Demo
from random import randint
from math import gcd
def EulerSieve(n):
vis = [True] * (n + 1)
primes = []
for i in range(2, n + 1):
if vis[i]:
primes.append(i)
for j in primes:
if i * j > n:
break
vis[i * j] = False
if i % j == 0:
break
return primes
def exgcd(a,b):
if b == 0:
return 1, 0, a
q, r = divmod(a, b)
x, y, d = exgcd(b, r)
return y, x - q * y, d
def elliptic_add(p, q, a, b, m):
if p[2] == 0:
return q
if q[2] == 0:
return p
if p[0] == q[0]:
if (p[1] + q[1]) % m == 0:
return 0, 1, 0
num = (3 * p[0] * p[0] + a) % m
denom = (2 * p[1]) % m
else:
num = (q[1] - p[1]) % m
denom = (q[0] - p[0]) % m
inv, _, g = modular_inv(denom, m)
if g > 1:
return 0, 0, denom
z = (num * inv * num * inv - p[0] - q[0]) % m
return z, (num * inv * (p[0] - z) - p[1]) % m, 1
def elliptic_mul(k, p, a, b, m):
res = (0, 1, 0)
while k > 0:
if p[2] > 1:
return p
if k % 2 == 1:
res = elliptic_add(p, res, a, b, m)
k = k // 2
p = elliptic_add(p, p, a, b, m)
return res
def lenstra(n, limit):
g = n
while g == n:
q = randint(0, n - 1), randint(0, n - 1), 1
a = randint(0, n - 1)
b = (q[1] * q[1] - q[0] * q[0] * q[0] - a * q[0]) % n
g = gcd(4 * a * a * a + 27 * b * b, n)
if g > 1:
return g
primes=EulerSieve(limit)
for p in primes:
pp = p
while pp < limit:
q = elliptic_mul(p, q, a, b, n)
if q[2] > 1:
return gcd(q[2], n)
pp = p * pp
return False
def main():
n=998244354
B=7000
curve_count=200
for i in range(curve_count):
res=lenstra(n, B)
if res != False:
print(res)
break
if __name__ == '__main__':
main()
一些优化
B的选定
如果定义
其中
我们希望最小化
如果采用近似
可以用牛顿迭代一类的方法找极值,当然通常使用经验对
多曲线
我们可以一次性生成多条曲线,一起在这多条曲线上做加法,这样只要有一个曲线上找到了因子就可以退出
求逆元的时候也可以先求一次然后然后用乘法来算其他逆元,你一个个求要快
蒙哥马利取模
Min_25 的博客有过介绍
核心步骤Reduce: (
def reduce(x):
m=((x%n)*(inv(r)))%n
x=(x-m*r)//n
if x>=r:
return x-r
return x
究其本质大概是在
大量乘法取模的时候实际效率很高
C++实现
太菜了写不动这个东西,给一份 zball 大佬的实现,效率极高
Here
0.20788920s consumed
200000000000000003 300000000000000011
比 factor 快上百倍、
同时 wikipedia 上也列举了许多 ecm 的高效实现,可以在 Wikipedia中查看(需独特的上网技巧)
Asusetic eru quionours