問題
合成数 $N$ が与えられる。$N$ の 非自明な約数($1<f<N$ かつ $f \mid N$)をひとつ出力せよ(複数ある場合はどれでもよい)。
入力形式
N
制約
| # | 条件 |
|---|---|
| 1 | $N = p \cdot q$($p, q$ は相異なる素数)、$10^{30} < q$、$p \le 10^{12}$、$N < 10^{60}$ |
| 2 | すなわち 「大きな数だが、片方の素因数は中くらい」 という状況(暗号鍵ではなく、乱数や積で作られた数の因数分解でよく現れる形) |
入出力例
入力例1
998244359987710471出力例1
998244353($998244359987710471 = 998244353 \times 1000000007$。どちらを出力してもよい)
この問題の背景: H. W. Lenstra Jr.「Factoring integers with elliptic curves」(Annals of Mathematics 126, 1987、https://doi.org/10.2307/1971363 、2026-09-24 参照)。実用実装は GMP-ECM(https://gitlab.inria.fr/zimmerma/ecm )で、「$p$ が 20 桁なら $B_1=11000$ で約 90 曲線」のような桁数別の推奨表が README にある。本問の $p\le10^{12}$(12 桁)は同表の「15 桁: $B_1=2000$、25 曲線」の手前にあたる。
概念図:mod N の計算は mod p と mod q を同時に行っている
ヒント(クリックで段階的に開示)
ヒント1(方向性)
Pollard の $p-1$ 法(Day148 Q4)は「$p-1$ が小さい素数の積(滑らか)なら $a^{M}\equiv1 \pmod p$ で $\gcd$ から $p$ が出る」が、$p-1$ が滑らかでないと その $p$ に対しては永久に効かない。群 $\mathbb{Z}_p^\times$ の位数 $p-1$ が固定だからである。位数を変えられる別の群があれば、滑らかな位数が出るまで引き直せる。
ヒント2(アプローチ)
楕円曲線 $E: y^2 = x^3 + ax + b$ の $\bmod p$ での点は群をなし、位数 $\#E(\mathbb{F}_p)$ は $p+1\pm2\sqrt p$ の範囲で 曲線 $(a,b)$ ごとに変わる(Hasse)。$N$ を法として点 $P$ に $M=\mathrm{lcm}(1..B_1)$ を掛ける計算をすると、$\bmod p$ では無限遠点になるが $\bmod q$ ではならない瞬間に、傾きの分母が $p$ の倍数で $q$ の倍数でない状態になり、$\gcd(\text{分母}, N)=p$ が出る。
ヒント3(誘導)
def inv(x, n): # 逆元が無ければ因数が見つかったことにする
g = gcd(x % n, n)
if g != 1: raise Found(g)
return pow(x, -1, n)
# 曲線を乱択: x, y, a を選び b = y^2 - x^3 - a x (mod N) にすれば P=(x,y) が乗る
# ステージ1: for 素数 p <= B1: P = (p^e) * P (p^e <= B1 の最大べき)
# 加法・2倍算の中で inv が失敗 → Found(g)。1 < g < N なら答え、g == N なら別の曲線模範解答 (Python)
模範解答を表示する(まず自力で解いてから)
import sys, random, math
class Found(Exception):
def __init__(self, f):
self.f = f
def inv(x, n):
x %= n
g = math.gcd(x, n)
if g != 1:
raise Found(g) # g == n のときは呼び出し側が「この曲線は失敗」と扱う
return pow(x, -1, n)
def ec_add(P, Q, a, n):
"""アフィン Weierstrass 曲線 y^2 = x^3 + a x + b 上の加法(mod n)。None は無限遠点。"""
if P is None: return Q
if Q is None: return P
x1, y1 = P; x2, y2 = Q
if x1 == x2:
if (y1 + y2) % n == 0:
return None
lam = (3 * x1 * x1 + a) * inv(2 * y1, n) % n
else:
lam = (y2 - y1) * inv(x2 - x1, n) % n
x3 = (lam * lam - x1 - x2) % n
return (x3, (lam * (x1 - x3) - y1) % n)
def ec_mul(k, P, a, n):
R = None
while k:
if k & 1:
R = ec_add(R, P, a, n)
P = ec_add(P, P, a, n)
k >>= 1
return R
def primes_upto(B):
s = bytearray([1]) * (B + 1); s[0] = s[1] = 0
for i in range(2, int(B ** 0.5) + 1):
if s[i]:
s[i * i::i] = bytearray(len(s[i * i::i]))
return [i for i in range(B + 1) if s[i]]
def ecm(n, B1=2000, curves=1000, rng=random.Random(159)):
ps = primes_upto(B1)
for _ in range(curves):
x = rng.randrange(n); y = rng.randrange(n); a = rng.randrange(n)
b = (y * y - x * x * x - a * x) % n # P=(x,y) が曲線に乗るよう b を決める
g = math.gcd(4 * a * a * a + 27 * b * b, n) # 特異曲線を除く
if 1 < g < n:
return g
if g == n:
continue
P = (x, y)
try:
for p in ps: # ステージ1: P <- lcm(1..B1) * P
pk = p
while pk * p <= B1:
pk *= p
P = ec_mul(pk, P, a, n)
if P is None:
break # mod n でも無限遠点 = 両方の素因数で同時に消えた
except Found as e:
if 1 < e.f < n:
return e.f
# e.f == n: 両方の法で同時に無限遠点。曲線を替える
return None
def main():
n = int(sys.stdin.readline())
if n % 2 == 0:
print(2); return
f = ecm(n)
print(f)
main()
計算量(ヒューリスティック): 素因数 $p$ を見つけるまでの期待コストは $\exp\big((\sqrt2+o(1))\sqrt{\ln p \ln\ln p}\big)$ 回の曲線演算(Lenstra 1987 の主定理、滑らかな数の分布に関する予想を仮定)。$N$ の大きさではなく $p$ の大きさで決まる のが $p-1$ 法・Pollard $\rho$($O(\sqrt p)$)との比較点。1 曲線あたりの点演算は $O(B_1)$ 回の加法(各加法に $\bmod N$ の逆元 1 回)。
Step-by-Step 解説
$p-1$ 法は $a^{\mathrm{lcm}(1..B_1)} \equiv 1 \pmod p$ を狙う。$\mathbb{Z}_p^\times$ の位数 $p-1$ が $B_1$-滑らかでなければ失敗し、$B_1$ を上げる以外に手がない。$p=2r+1$($r$ 素数)なら実質不可能。Lenstra の着想は「$\mathbb{Z}_p^\times$ の代わりに $E(\mathbb{F}_p)$ を使う。位数 $\#E(\mathbb{F}_p)\in[p+1-2\sqrt p,\ p+1+2\sqrt p]$ は曲線で変わるので、滑らかな位数を引くまで曲線を替えればよい」。
実際には $p$ を知らないので $\bmod N$ で計算する。CRT により $\bmod N$ の計算は $\bmod p$ と $\bmod q$ の計算を同時に行っているのと同じ。$M \cdot P$ が $\bmod p$ で無限遠点になる($\#E(\mathbb{F}_p) \mid M$)が $\bmod q$ ではならないとき、加法公式の分母($x_2-x_1$ または $2y_1$)が $p$ で割り切れ $q$ で割り切れない。その逆元計算で $\gcd(\text{分母}, N)=p$。「逆元が取れない」というエラーが答え になる。
$a,x,y$ を乱択して $b=y^2-x^3-ax$ とすれば $P=(x,y)$ が曲線に乗り、平方根計算が要らない。判別式 $4a^3+27b^2$ と $N$ の $\gcd$ が $1<g<N$ ならそれ自体が因数、$g=N$ なら特異なので捨てる。
$M=\mathrm{lcm}(1..B_1)$ を一度に掛けるのではなく、素数 $p\le B_1$ ごとに $p^e\le B_1$ の最大べきを掛ける。効果は同じで、巨大な $M$ を作らずに済み、途中で分母が $p$ の倍数になった時点で即座に止まる。
| $p$ の桁数 | $B_1$ | 成功までの曲線数 | ECM | Pollard $\rho$ |
|---|---|---|---|---|
| 8 | 2000 | 2 | 0.22 s | 0.02 s |
| 10 | 2000 | 16 | 1.53 s | 0.27 s |
| 12 | 2000 | 28 | 2.96 s | 3.77 s |
| 13 | 10000 | 31 | 10.7 s | (未実行。$\sqrt p\approx3\times10^6$ 反復の見込み) |
$p$ が 12 桁で $\rho$ と並び、それ以上で逆転する。$\rho$ は $O(\sqrt p)$ で $p$ が 1 桁増えると $\sqrt{10}\approx3.2$ 倍、ECM は準指数なので伸びが緩い。本問の制約 $p\le10^{12}$ は「$\rho$ でも通るが ECM の方が伸びが良い」境界に置いた。
迷った他案と採らなかった理由
| 案 | 計算量 | 採否 | 理由 |
|---|---|---|---|
| 試し割り | $O(p)$ | 不採用 | $p\le10^{12}$ で不可能 |
| Pollard $\rho$(Day017 Q3・Day041 Q5) | $O(\sqrt p)$ | 比較用 | $p=10^{12}$ で 3.77 s(実測)。$p$ が 13 桁を超えると数十秒〜分。制約を $p\le10^{11}$ に緩めるなら $\rho$ で十分で、その場合は実装の簡単な $\rho$ を選ぶ |
| Pollard $p-1$(Day148 Q4) | $O(B_1)$ | 不採用 | $p-1$ が滑らかなときだけ効く。乱択された $p$ では成功率が低く、制御できない |
| ECM ステージ 1 のみ・アフィン座標(本解) | 曲線数 × $O(B_1)$ 逆元 | 採用 | 40 行で書け、原理(逆元失敗=因数)が最も見えやすい。逆元 pow(x,-1,N) は Python 3.8+ で C 実装 |
| ECM + ステージ 2($B_1 < \ell \le B_2$ の素数 1 個を追加で許す) | ステージ 1 の数倍のコストで成功率が大幅に上がる | 扱わない | GMP-ECM の実力の源泉だが、本問の $p\le10^{12}$ ではステージ 1 だけで 30 曲線以内に収まった(実測)。前提が $p\ge10^{15}$ に変わったら必須 |
| Montgomery 曲線 + 射影座標(逆元なし) | 1 加法あたり乗算 ~10 回、逆元 0 回 | 扱わない | 実装の主流(GMP-ECM)。Python では多倍長乗算も逆元も C 実装なので差が小さく、本質を隠すので範囲外 |
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
pow(x, -1, n) の ValueError をそのまま落とす | 逆元が無い=因数発見、が本手法の核心 | 先に $\gcd$ を取り、$\ne1$ なら例外で因数を運ぶ |
| $\gcd = N$ を「失敗」ではなく答えにする | $\bmod p$ と $\bmod q$ で同時に無限遠点になった | 曲線を替えてやり直す |
$2P$ の判定を x1 == x2 だけにする | $P + (-P)$($y_1=-y_2$)は無限遠点 | (y1 + y2) % n == 0 なら None |
| $b$ を乱択して $P$ を曲線上に探す | 平方根 $\bmod N$ は $N$ の分解と同等に難しい | $x,y,a$ を選び $b$ を逆算する |
| $B_1$ を巨大にして 1 曲線で粘る | 滑らかでない位数の曲線は $B_1$ を上げても効かない | $B_1$ は桁数の目安(GMP-ECM の表)に固定し、曲線数で稼ぐ |
| $N$ が偶数のときに曲線演算に入る | inv(2*y1, n) が常に失敗 | 先に 2 で割る |
次のステップ
- 発展1: ステージ 2 を追加する——ステージ 1 の結果 $Q=M\cdot P$ に対し、$B_1<\ell\le B_2$ の各素数 $\ell$ について $\ell Q$ の $x$ 座標の差の積を溜めて最後に 1 回 $\gcd$ を取る("standard continuation")
- 発展2: Suyama のパラメトリゼーション(位数が必ず 12 の倍数になる曲線族)で成功率を上げ、曲線数の減りを実測する
- 扱わなかった範囲: Montgomery 形式 $By^2=x^3+Ax^2+x$ と $x$ 座標のみのラダー、複数の素因数を持つ $N$ の完全分解(再帰と Miller–Rabin の組合せ)、$p$ と $q$ が同程度の大きさ(RSA 型)の場合——それは ECM の守備範囲外で、数体篩(NFS)の領域
検証ログ(本セッションで実行した数値)
- 入力例1: 模範解答(乱数 seed 159、$B_1=2000$)は 13 曲線目で $1000000007$ を返した($N/f=998244353$)。検証スクリプト側(seed 5)は 24 曲線目で $998244353$。どちらも $N \bmod f = 0$ を確認——どの因数が先に出るかは曲線の乱択次第で、出力例はその一方
- 正当性: $p\in[10^6,10^8]$、$q\in[10^{30},10^{31}]$ の $N=pq$ を 60 個生成し $B_1=500$ で実行 → 失敗 0 件、返した値は全件 $N$ の非自明な約数。成功までの曲線数 平均 11.2、最大 61(合計 10.6 s)
- クロスオーバー(実測、$q\approx10^{40}$、$N$ は 49〜54 桁): 上表のとおり。$p$ が 12 桁で ECM 2.96 s vs $\rho$ 3.77 s、13 桁で ECM 10.7 s($\rho$ は未計測)
- 出所ラベル: 曲線数・秒数は実測(Python 3.14、
pow(x,-1,n)使用)。期待曲線数の理論式と GMP-ECM の $B_1$ 表は引用(本セッションで再導出していない)。「13 桁で $\rho$ が $3\times10^6$ 反復」は $\sqrt p$ からの導出値で未実測 - 検証スクリプト: スクラッチパッドの
q3.py