分解质因数之二次筛法

\(n\) 的所有质因子(\(n\ge 2\))。

Notation

\(\mathbb{N}:=\{0,1,2,3\ldots \}\) 表示自然数。

\(\mathbb{P}:=\{2,3,5,7\ldots \}\) 表示素数集。

\(\mathbb{P}^\infty:=\{2,2^2,2^3\ldots 3,3^2,3^3\ldots \}\) 表示素数的幂次构成的集合。

\(\mathbb{Z}\) 表示整数环。

\(\pi(x)\) 表示 \([1,x]\cap\mathbb{N}\) 中的素数个数。

我会暴力

注意到,如果 \(n\not\in\mathbb{P}\),那么其至少有一个因子 \(2\le d\le\lfloor\sqrt{n}\rfloor\),于是枚举 \(d\) 即可。显然是 \(O(\sqrt{n})\) 的。

注意力惊人

为了方便,我们首先排除 \(n\in\mathbb{P}^\infty\) 的情况,因为 \((\log n)^{\log n}>n\),所以这一步也就是 \(O(\log ^2n)\) 的(这里一个 \(\log\) 来自快速幂,某些更严格的 \(\text{unit cost}\) 会给出不同的代价,但是至多就是若干个 \(\log\),根据常识,分解因数目前没有 \(P\) 的算法所以这里一定不是瓶颈)。

此时我们就可以说 \(n=\prod\limits_{i=1}^kp_i^{\alpha_i}\),并且 \(k\ge 2,\alpha_i>0\)

我们在 \(\mathbb{Z}/n\mathbb{Z}\) 上讨论,注意到此时 \(\sqrt1\) 不只是 \(1,-1\),还存在别的解。具体地,假设 \(n=ab,\gcd(a,b)=1\),那么

\[\begin{cases} &x^2\equiv 1\pmod a\\ &x^2\equiv 1\pmod b \end{cases} \]

分别有 \(\pm1\) 两个解,用中国剩余定理可以合并出四个 \(\bmod n\) 下本质不同的解,所以一定存在 \(u\in \mathbb{Z}/n\mathbb{Z}\),使得 \(u\not\equiv\pm 1\pmod n,u^2\equiv 1\pmod n\)

不难注意到,\(1^2\equiv u^2\pmod n\Rightarrow n|(1-u)(1+u)\),并且因为 \(u\mp1\not\equiv0\pmod n\),从而只能是 \(\gcd(n,1-u),\gcd(n,1+u)>1\),于是求解 \(\gcd\) 即可求得因数。

所以我们的目标就变成了求解 \(u\)。这里因为没有别的信息所以一般都选择随机化,从 \((1,n)\cap\mathbb{N}\) 里随机选一个数 \(x\),如果 \(\gcd(x,n)>1\),那么已经找到一个非平凡因子;否则 \(x\)\(\bmod n\) 下存在逆元,用同样的办法找一个 \(y\),若有 \(x^2\equiv y^2\pmod n\),就有 \(y\in\{x,-x,ux,-ux\}\)(如果 \(n\) 不止两个素因子,这里可能的情况会更多),其中只有 \(x,-x\) 是平凡的情况,于是至少有 \(\frac 12\) 的概率可以得到 \(u\)

Pollard Rho

非常 \(\text{naive}\) 的,我们随机取上述的 \(x\),得到序列 \(\{x_i\}\),重复直到存在 \(x_i^2\equiv x_j^2\pmod n\)。根据之前的分析,这个流程期望最多只需要重复 \(2\) 次。

然后我们考虑这个序列的期望长度,首先可能的平方值个数 \(\le n\)。假设已经有了 \(i\) 个这样的 \(x\) 并且还没有发生碰撞,那么此时碰撞的概率 \(\ge\frac in\)

\(L\) 表示序列 \(\{x_i\}\) 的长度,就有 \(P(L>i)\le \prod\limits_{j=1}^i(1-\frac jn)\),运用不等式 \(1+x\le e^x\) 就有

\[P(L>i)\le\prod_{j=1}^ie^{-\frac jn}=\exp(-\sum_{j=1}^i\frac jn)=\exp(-\frac{i(i+1)}{n}) \]

\(i=\sqrt n\) 就可以保证 \(O(1)\) 的成功率,这么实现和暴力就差不多了,也是 \(O(\sqrt n)\) 的。

这时,我们需要使用一点技巧来提高效率,这里我们选择重新分析复杂度(并且进行细节上的修改)。上面的说法可能比较类似把所有 \(x\) 存到一个 \(\text{hash table}\) 里,一直存直到碰撞,这样的话的确是 \(O(\sqrt n)\) 的。

但是还有一种做法类似贪心,先生成 \(k\)\(x\),这里的 \(k\) 可以任取(但是太小很显然不能保证有碰撞)。然后从其中选出 \(m\)\((x_i,x_j)\),直接判断 \(\gcd(x_i-x_j,n)\)\(\gcd(x_i+x_j, n)\)

\(p\)\(n\) 最小的素因子,那么上面的复杂度应当是 \(O(p)\) 的,因为随机取数期望 \(O(\sqrt p)\) 次就可以得到数对 \((x_i,x_j)\) 满足 \(\gcd\) 不为 \(1\)。不过这样依然有 \(O(\sqrt n)\) 的复杂度(考虑一对孪生素数即可)。

我们的目标是在 \(\mathbb{Z}/p\mathbb{Z}\) 中找到一对碰撞的 \((x_i,x_j)\),也就是使得 \(x_i-x_j\equiv 0\pmod p\)。这个时候我们又有了一个 \(\text{naive}\) 的想法,我们找一个有周期的随机函数,这样生成出来的随机数就是形如 \(x_1\rightarrow x_2\rightarrow\ldots x_a \rightarrow x_{a+1}\rightarrow\ldots x_{a+b}\rightarrow x_a\rightarrow\ldots\) 这样的 "\(\rho\)" 形函数。

根据大量的尝试,高于一次的多项式函数几乎都可以符合这个性质。这里真的没有证明了,因为人类目前还不会证(虽然有一些不够完美的结论,比如这里)。最常用的例子就是 \(g(x)=x^2+c\),这里的 \(c\)\((1,n)\) 中的随机数,也就是 \(x_{i+1}=g(x_i)\)\(x_1\) 也是 \((1,n)\) 中的随机数。

如果函数是真的随机的话,证明其中循环的长度是 \(O(\sqrt p)\) 就是之前分析的碰撞概率,碰撞了显然是循环。

其实从 \([1,n]\) 中随机选择 \(x_i\) 也是对的。

Quadratic Sieve

思路

我们考虑 \(B\)-\(\text{smooth}\) 的那些数,也就是所有素因子都不超过 \(B\) 的数(\(B\) 是一个任选的常数)。

定义 \(\Psi(x;B)=\#\{1<n\le x:\forall p|n\land p\in \mathbb{P},p\le B\}\),其中 \(\#\) 表示对集合大小计数(这大概是解析数论中比较常用的记号,反正和 \(||\) 也没区别)。

我们考虑选取一个表达式 \(Q(s)=s^2+cn\equiv s^2\pmod n\),然后通过某种办法选出 \(k\) 个数 \(\{s_i\}\) 使得 \(Q(s_i)\)\(B\)-\(\text{smooth}\) 的。

此时我们可以列出 \([1,B]\cap\mathbb{N}\) 中的 \(\pi(B)\) 个素数,每个 \(Q(s_i)\) 都可以写成

\[Q(s_i)=p_1^{\alpha_{i,1}}p_2^{\alpha_{i,2}}\ldots p_{\pi(B)}^{\alpha_{i,\pi(B)}} \]

的形式,而我们已经保证 \(Q(s_i)\equiv s^2\pmod n\) 了,我们想要的就是找到一个指标集 \(S\subset\{1,2,3\ldots k\}\) 使得 \(\prod\limits_{i\in S}Q(s_i)\equiv m^2\pmod n\),这样 \((\prod\limits_{i\in S}s_i, m)\) 就是一个 \(\bmod n\) 下平方相等的数对。

不难想到我们只关心那些不是平方的因子,也就是我们只关心 \(\alpha_{i,j}\bmod 2\) 的值。不妨把一个 \(Q(s_i)\) 的素因子指数在 \(\bmod 2\) 意义下看成一个 \(\pi(B)\) 维向量,那么显然只要有 \(\pi(B)+t\)\(Q(s_i)\),我们就可以找到至少 \(t\) 组本质不同的线性组合使得其为全 \(0\) 向量,也就是平方。(这里其实不用说本质不同,因为在 \(\bmod 2\) 的意义下系数只可能是 \(\{0,1\}\)

根据之前的分析,这样就有至少 \(1-\frac{1}{2^t}\) 的概率成功得到 \(n\) 的一个因子。实际上一般取一个 \(10\sim20\)\(t\),然后 \(k=\pi(B)+t\) 即可。

细节与复杂度

很显然,上面的算法主要有两步,找 \(B\text{-smooth number}\) 和进行高斯消元。

高斯消元的时候因为一般可以认为 \(t=O(1)\),所以是 \(O(\pi^3)\) 的(这里的 \(\pi\)\(\pi(B)\) 的缩写)。当然如果你能注意到一个数 \(x\) 最多有 \(\log x\) 个不同的素因子,那么可以用 Berlekamp–Massey 算法做到 \(O(\pi^2\min\{\log n,\pi(B)\})\) 的复杂度。结合后面比较繁杂的分析可以说明认为 \(\log n\ll\pi(B)\) 不太会导致问题。

\(B\text{-smooth}\) 数主要有两步,第一个是检验,暴力做的话是试除 \(\pi(B)\) 个小素数,就是 \(O(\pi(B))\) 的。但是假如我们知道要筛的区间(这也是为什么这个算法叫筛法),比如是 \(s\in [L,R]\),因为 \(Q(s)=s^2+cn\),我们可以预先求出 \(s^2+cn\equiv 0\pmod p\) 的根 \(r_1,r_2\),只有形如 \(r_1+kp,r_2+kp\)\(s\) 才是 \(p\) 的倍数。

但是这样筛了我们还是要暴力试除,于是我们考虑启发式地选取,对于一个 \(Q(s)\),我们已经知道其有若干因子 \(q_1,q_2\ldots q_l\),那么就有

\[Q(s)=\prod\limits_{i=1}^lq_i^{e_i}\Leftrightarrow \log Q(s)=\sum_{i=1}^l e_i\log q_i \]

一个质朴的想法是考虑到 \(\sum \frac 1{p^2}\) 收敛,于是钦定 \(e_i=1\) 也只是需要 \(O(1)\) 倍次检验。当然实际上处于常数考虑一般是只检测那些 \(\log Q(s)\ge \alpha\sum\limits_{i=1}^l{q_i}\)\(s\)\(\alpha\) 一般取 \(1.2\sim 1.5\) 的常数。

求解 \(Q(s)\equiv 0\pmod p\) 则就是开方而已,在模意义下可以用这里提供的算法,用其中复杂度比较优秀的就可以做到 \(O((R-L)\log\pi(B))\)。(朴素的暴力显然是 \(O((R-L)\pi(B))\) 的)

第二步就是每次找有多大的概率成功,这个和 \(B\text{-smooth}\) 数的分布是有关的,设 \(m=\max\{Q(s_i)\}\),那么成功的概率就是 \(\frac{\Psi(m;B)}{m}\)(希望你还记得 \(\Psi(m;B)\) 表示 \(\le m\)\(B\text{-smooth}\) 数的数量)。上面的 \(R-L\) 期望上就是 \(\frac{m}{\Psi(m;B)}\)

这里发挥一下注意力,不难想到,取 \(s=\sqrt{-cn}+O(1)\) 可以使得 \(Q(s)=s^2=O(\sqrt n)\)。这样比直接取 \(m=O(n)\) 要好不少。(实际上中间这些步骤我也没有全部试过,有空的人可以告诉我那些取法能比 \(\text{pollard rho}\) 优秀)

整体的复杂度合起来就好了。得到

\[O\Big((\pi(B))^2\log n+\frac{\pi(B)\sqrt n}{\Psi(n;B)}\Big) \]

其中 \(\pi(B)=\frac{B}{\log B}\) 是大家熟知的结果,难点在于 \(\Psi(n;B)\) 的求解。

省流:\(u=\log_B n,\Psi(n;B)=u^{-u(1+o(1))}\)

根据省流的知识,我们再经过一番辛苦的初等代数运算便可以知道 \(B=\exp(\sqrt{2\log n\log\log n})\) 时取得最小值,总复杂度最优值为 \(O(\exp(\sqrt{(2+o(1))\log n\log\log n}))\)

至于这个定理的证明,那下次再说,口古口古……

给出一个非常 \(\text{naive}\)python 实现,常数非常巨大,即使在 python 中也喜提最劣解。

from math import floor, sqrt, log, gcd
from random import randint

def mod_num(x : int, mod : int) -> int:
  if x >= mod: x %= mod
  elif x < 0: x -= (x // mod) * mod
  return x

def mod_add(x : int, y : int, mod : int) -> int:
  z = x + y
  return z if z < mod else z - mod

def mod_mul(x : int, y : int, mod : int) -> int:
  z = x * y
  if z >= mod: z %= mod
  return z

def mod_pow(base : int, power : int, mod : int) -> int:
  base = mod_num(base, mod)
  if base == 0 or base == 1: return base
  ret = 1
  while power > 0:
    if power % 2 == 1: ret = mod_mul(ret, base, mod)
    base = mod_mul(base, base, mod)
    power //= 2
  return ret

class prime_checker:
  def __init__(self, small_prime_bound : int = 37):
    self.small_prime = [
      x for x in range(2, small_prime_bound + 1) if self.bf_checker(x)
    ]

  def bf_checker(self, x : int) -> bool:
    if x == 1: return False
    if x % 2 == 0: return x == 2
    bound = floor(sqrt(x)) + 1
    for i in range(3, bound, 2):
      if x % i == 0:
        return False
    return True

  def fermat_test(self, x : int) -> bool:
    if x == 1: return False
    if x % 2 == 0: return x == 2
    a = randint(2, x - 1)
    return mod_pow(a, x - 1, x) == 1
 
  def quadratic_test(self, x : int, a : int | None = None) -> bool:
    if x == 1: return False
    if x % 2 == 0: return x == 2
    if a is None: a = randint(2, x - 2)
    if mod_pow(a, x - 1, x) != 1: return False # fermat ctest
    d, s = x - 1, 0
    while d % 2 == 0:
        d //= 2
        s += 1
    t = mod_pow(a, d, x)
    if t == 1 or t == x - 1:
        return True
    for _ in range(1, s):
        t = mod_mul(t, t, x)
        if t == x - 1:
            return True
    return False

  def miller_rabin_test(self, x : int, times : int = 0) -> bool:
    if x == 1: return False
    if x in self.small_prime: return True
    for p in self.small_prime:
      if not self.quadratic_test(x, p):
        return False
    for i in range(times):
      if not self.quadratic_test(x):
        return False
    return True

def generate_random_prime(l : int, r : int) -> int:
  if r < l: raise ValueError(f"The range is illegal.")
  checker = prime_checker()
  for _ in range(10000):
    x = randint(l, r)
    if checker.miller_rabin_test(x): return x
  raise RuntimeError(f"Cannot find prime in [{l}, {r}], maybe there's no prime or need more test")

def generate_factor_hard_number(r : list[tuple[int, int]]) -> tuple[int, list[int]]:
  ret = 1
  lst = []
  for L, R in r:
    x = generate_random_prime(L, R)
    lst.append(x)
    ret *= x
  return (ret, lst)

class image_number:
  def __init__(
    self,
    mod : int,
    non_quadratic_residue : int,
    num : tuple[int, int] = (0, 0)):
    self.mod = mod
    self.non_quadratic_residue = non_quadratic_residue
    self.number = num
    # A+Bi, i^2 = non quadratic residue

  def __mul__(self, other : "image_number") -> "image_number":
    mod = self.mod
    square = self.non_quadratic_residue
    a, b = self.number
    c, d = other.number
    e = mod_num(a * c + b * d * square, mod)
    f = mod_num(b * c + a * d, mod)
    return image_number(mod, square, (e, f))

  def __add__(self, other : "image_number") -> "image_number":
    mod = self.mod
    square = self.non_quadratic_residue
    a, b = self.number
    c, d = other.number
    e, f = mod_num(a + c, mod), mod_num(b + d, mod)
    return image_number(mod, square, (e, f))

  def __pow__(self, other : int) -> "image_number":
    mod = self.mod
    square = self.non_quadratic_residue
    ret = image_number(mod, square, (1, 0))
    base = image_number(mod, square, self.number)
    while other > 0:
      if other % 2 == 1: ret = ret * base
      other //= 2
      base = base * base
    return ret

class quadratic_residue:    
  def __init__(self, safety : bool = True):
    self.safety = safety

  def is_quadratic_residue(self, x : int, p : int) -> bool:
    """判断 x 是否是 mod p 下的二次剩余"""
    return mod_pow(x, (p - 1) // 2, p) == 1  # 勒让德符号

  def _generate_non_quadratic_residue(self, a : int, p : int) -> tuple[int, int]:
    """
    找到 n 使得 n^2-a 是 mod p 的非二次剩余,返回 (n, n^2-a)
    """
    # 随机选取的成功率是 1/2
    while True:
      n = randint(1, p - 1)
      num = mod_num(n * n - a, p)
      if not self.is_quadratic_residue(num, p):
        return (n, num)

  def solve_equation(self, a : int, p : int) -> list[int]:
    """
    使用 Cipolla 算法,求出 x^2\equiv a\pmod p 在 \mathbb{Z}_p 下的所有解
    无解时返回 [] 而不是 None。
    保证返回的解有序。
    """
    if self.safety and not prime_checker().miller_rabin_test(p):
      raise TypeError(f"{p} is not a prime number!")
    if a == 0: return [0]
    if not self.is_quadratic_residue(a, p): return []
    # corner case
    r, i2 = self._generate_non_quadratic_residue(a, p)
    # i2 = i ^2, x = (r+i)^\frac{p+1}{2}
    base = image_number(p, i2, (r, 1))
    x = (base ** ((p + 1) // 2)).number[0]  # 可以保证虚部为 0
    return sorted([x, p - x])  # 还是保证有序好用一点

class quadratic_sieve:
  # 存在把 (B, B^2) 间素数存下来的优化,但是似乎没有更快
  def __init__(self, B_smooth, alpha : float = 0.7):
    self.B = B_smooth  # 找 B smooth 的数
    self.alpha = alpha  # 要求 \sum log p >= alpha * log |Q|
    checker = prime_checker()
    self.small_prime = [2] + [
      x for x in range(3, B_smooth + 1, 2) if checker.miller_rabin_test(x)
    ]
    self.smooth_num : list[tuple[int, int]] = []
    # 同时存放了 s 和 Q(s)
    self.smooth_num_p : list[int] = []
    # list[int],只用 0/1 表示是否不是平方数,1 表示次数为奇数,用 int 压位处理以提速
    self.smooth_num_exp = []
    # 完整的素因数指数列表
    self.qr_solver = quadratic_residue(safety=False)
    self.prime_test = checker
    self.quadratic_param = (0, 0, 0)
    self.quadratic_extra = (0, 0, 0)

  def _make_quadratic_param(self, n : int, M : int) -> int:
    """
    生成 Ax^2+Bx+C 使得 B^2/4-AC=n,这样 x\sim\sqrt n 时,Q(s) 更小
    因为此时 Ax^2+Bx+C=\frac{(Ax+B)^2-n}{A}
    额外保存: D, B0, E, Q(s) ≡ (E + D*s)^2 (mod n)
    """
    """basic method"""
    # sn = floor(sqrt(n))
    # A = 1
    # B = 2 * sn
    # C = sn * sn - n
    # self.quadratic_param = (A, B, C)
    """MPQS method"""
    target_D = max(3, int(sqrt(sqrt(2.0 * n) / M)))
    while True:
      L = max(3, target_D // 2)
      R = max(L + 10, target_D * 2)
      D = generate_random_prime(L, R)
      if gcd(n, D) != 1:
        if n % D == 0:
          raise RuntimeError(f"Found factor directly: {D}")
        continue
      roots = self.qr_solver.solve_equation(n % D, D)
      if roots: break
    A = D * D
    cand = []
    for y0 in roots:
      if y0 % D == 0: continue
      t = ((n - y0 * y0) // D) % D
      inv = mod_pow((2 * y0) % D, D - 2, D)
      k = (t * inv) % D
      B0 = y0 + k * D
      for b in (B0, A - B0):
        C = (b * b - n) // A
        cand.append((abs(C), b, C))
    _, B0, C = min(cand)
    E = mod_num(B0 * pow(D, -1, n), n)
    self.quadratic_param = (A, 2 * B0, C)
    self.quadratic_extra = (D, B0, E)
    # Hensel lifting

  def _calc_quadratic_formula(self, s : int) -> int:
    A, B, C = self.quadratic_param
    return A * s * s + B * s + C
  def _msb_pos(self, x: int) -> int:
    return x.bit_length() - 1
  def _is_smooth(self, x : int) -> bool:
    for p in self.small_prime:
      while x % p == 0:
        x //= p
    return x == 1

  def _factor_exponents(self, x: int) -> tuple[bool, dict[int, int], int]:
    """
    返回 (是否 B-smooth, 指数表, 符号) sign = 0/1 表示是否含有 -1
    """
    exps = {}
    sign = 0
    if x < 0:
      sign = 1
      x = -x
    for p in self.small_prime:
      cnt = 0
      while x % p == 0:
        x //= p
        cnt += 1
      if cnt:
        exps[p] = cnt
    return (x == 1, exps, sign)

  def _roots_mod_prime(self, n: int, p: int) -> list[int]:
    A, _, C = self.quadratic_param
    D, B0, _ = self.quadratic_extra
    if p == 2: return [C & 1]
    a = A % p
    if a == 0: return []
    roots = self.qr_solver.solve_equation(mod_num(n, p), p)
    if not roots: return []
    inv_a = mod_pow(a, p - 2, p)
    r1 = mod_num((roots[0] - B0) * inv_a, p)
    r2 = mod_num((roots[-1] - B0) * inv_a, p)
    if r1 == r2: return [r1]
    return [r1, r2]

  def _find_smooth(self, n : int) -> None:
    """针对指定的 n 生成一定数量的 B-smooth number"""
    self.smooth_num.clear()
    self.smooth_num_p.clear()
    self.smooth_num_exp.clear()
    # 清空历史数据
    need_num = len(self.small_prime) + 20  # 冗余 20 个,如此可能导致对小的情况需要暴力,解不出来的概率只有 1e-6
    search_num = need_num * 50  # 经验公式,乘的是 smooth number 出现概率
    self._make_quadratic_param(n, search_num)  # 参数和搜索长度有关
    A, B, C = self.quadratic_param
    D, B0, E = self.quadratic_extra
    # MPQS 生成表达式
    # 这些是 basic method 用的
    sieve_score = [0.0] * search_num
    for p in self.small_prime:
      roots = self._roots_mod_prime(n, p)
      if not roots: continue
      lp = log(p)
      for r in roots:
        for s in range(r, search_num, p):
          sieve_score[s] += lp
    for s in range(search_num):
      q = self._calc_quadratic_formula(s)
      if q == 0: continue
      if sieve_score[s] < self.alpha * log(abs(q)): continue
      ok, exps, sign = self._factor_exponents(q)
      if not ok: continue  # 暴力检测发现不是 smooth 的
      mask = sign
      for i, p in enumerate(self.small_prime, start=1):
        if exps.get(p, 0) & 1:
          mask |= (1 << i)
      self.smooth_num.append((s, q))
      self.smooth_num_p.append(mask)
      self.smooth_num_exp.append((exps, sign))
      if len(self.smooth_num) >= need_num: return
    # 判断 Q(s) 是否生成了一个 B-smooth num,如果是,则加入 self.smooth_num 并建立对应的 smooth_num_p
    # 可能不够,此时自动填充
    offset = search_num
    while len(self.smooth_num) < need_num:
      extra = search_num
      sieve_score = [0.0] * extra
      for p in self.small_prime:
        roots = self._roots_mod_prime(n, p)
        if not roots: continue
        lp = log(p)
        for r in roots:
          start = (r - offset) % p
          for t in range(start, extra, p):
            sieve_score[t] += lp
      for t in range(extra):
        s = offset + t
        q = self._calc_quadratic_formula(s)
        if q == 0: continue
        if sieve_score[t] < self.alpha * log(abs(q)): continue
        ok, exps, sign = self._factor_exponents(q)
        if not ok: continue
        mask = sign
        for i, p in enumerate(self.small_prime, start=1):
          if exps.get(p, 0) & 1: mask |= (1 << i)
        self.smooth_num.append((s, q))
        self.smooth_num_p.append(mask)
        self.smooth_num_exp.append((exps, sign))
        if len(self.smooth_num) >= need_num: return
      offset += extra

  def _find_dependencies(self) -> list[list[int]]:
    """
    返回所有线性依赖,得到 X^2 - Y^2 \equiv 0 \pmod p
    """
    basis = {}
    deps = []
    for idx, vec0 in enumerate(self.smooth_num_p):
      vec = vec0
      comb = 1 << idx
      while vec:
        pivot = self._msb_pos(vec)
        if pivot not in basis:
          basis[pivot] = (vec, comb)
          break
        bvec, bcomb = basis[pivot]
        vec ^= bvec
        comb ^= bcomb
      if vec == 0:
        dep, j, tmp = [], 0, comb
        while tmp:
          if tmp & 1: dep.append(j)
          tmp >>= 1
          j += 1
        deps.append(dep)
    return deps

  def _build_xy(self, n: int, dep: list[int]) -> tuple[int, int]:
    X = 1
    total_exp = {p: 0 for p in self.small_prime}
    total_sign = 0
    for idx in dep:
      s, q = self.smooth_num[idx]
      D, B0, E = self.quadratic_extra
      X = mod_num(X * mod_num(E + D * s, n), n)
      exps, sign = self.smooth_num_exp[idx]
      total_sign ^= sign
      for p, e in exps.items():
        total_exp[p] += e
    # 理论上 total_sign 必须是 0
    if total_sign != 0: raise ValueError("Invalid dependence")
    Y = 1
    for p, e in total_exp.items():
      if e: Y = mod_mul(Y, mod_pow(p, e // 2, n), n)
    return X, Y

  def _bf_factorize(self, n : int) -> list[int]:
    """对 <= B^2 的数进行暴力分解"""
    ret = []
    for p in self.small_prime:
      while n % p == 0:
        ret.append(p)
        n //= p
    if n != 1: ret.append(n)  # 保证此时的 n 是 (B, B^2) 中的素数
    return ret

  def factorize(self, n : int) -> list[int]:
    """从小到大返回 n 的所有因数"""
    if n <= 0: raise ValueError(f"Cannot factorize {n} less than 1")
    if n == 1: return [1]
    elif self.prime_test.miller_rabin_test(n): return [n]
    elif n <= self.B ** 2: return sorted(self._bf_factorize(n))
    else:
      r = floor(sqrt(n))
      if r * r == n: return sorted(self.factorize(r) * 2)
    self._find_smooth(n)
    deps = self._find_dependencies()
    if not deps: raise RuntimeError("Failed to find a linear dependence.")
    for dep in deps:
      X, Y = self._build_xy(n, dep)
      g1 = gcd(mod_num(X - Y, n), n)
      g2 = gcd(mod_num(X + Y, n), n)
      for g in [g1, g2]:
        if g <= 1 or g >= n: continue
        return sorted(self.factorize(g) + self.factorize(n // g))
    raise RuntimeError("Dependency found but gcd was trivial; need another dependency.")

还有不少可以优化的地方,不过我常数太大了,测试结果可能不正常()。想知道的可以看这里,不过其中的 \(3\) 我本地没有跑出优化效果,我的代码里面实现了其中的第 \(1,4\) 个优化。

posted @ 2026-03-09 23:52  嘉年华_efX  阅读(6)  评论(0)    收藏  举报