Day 159-Q3 — 2026-09-24 — Lenstra の楕円曲線法(ECM・「群の位数を曲線ごとに引き直せる」Pollard p−1)

2026-09-24 赤色 Master (Phase 8+) ★★★★★★★★★ 巨大な合成数から「中くらいの素因数」だけを狙い撃ちする。$\mathbb{Z}_N$ 上で楕円曲線の加法を実行し、逆元計算の失敗から因数を得る

問題

合成数 $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 を同時に行っている

N = p·q。曲線 E 上で M·P(M = lcm(1..B1))を mod N で計算する mod p(見えない世界) #E(F_p) ∈ [p+1−2√p, p+1+2√p] 曲線を替えるたびに位数が変わる 位数が B1-滑らか → M·P = O(無限遠点) 途中の加法で分母 (x2−x1 または 2y1) ≡ 0 (mod p) p−1 法は位数 p−1 が固定 → 引き直せない mod q(q は巨大) #E(F_q) は ~q で、滑らかである確率は ≈ 0 同じ加法の分母 ≢ 0 (mod q) → 分母は p の倍数だが q の倍数でない pow(分母, −1, N) が失敗 → gcd(分母, N) = p 「逆元が取れない」というエラーが答え。実測: p=12 桁で 28 曲線・2.96 s

ヒント(クリックで段階的に開示)

ヒント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 解説

1$p-1$ 法の限界と「群を変える」発想

$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]$ は曲線で変わるので、滑らかな位数を引くまで曲線を替えればよい」。

2$\bmod N$ で曲線演算をする意味

実際には $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$。「逆元が取れない」というエラーが答え になる。

3曲線の選び方

$a,x,y$ を乱択して $b=y^2-x^3-ax$ とすれば $P=(x,y)$ が曲線に乗り、平方根計算が要らない。判別式 $4a^3+27b^2$ と $N$ の $\gcd$ が $1<g<N$ ならそれ自体が因数、$g=N$ なら特異なので捨てる。

4ステージ 1 の掛け算

$M=\mathrm{lcm}(1..B_1)$ を一度に掛けるのではなく、素数 $p\le B_1$ ごとに $p^e\le B_1$ の最大べきを掛ける。効果は同じで、巨大な $M$ を作らずに済み、途中で分母が $p$ の倍数になった時点で即座に止まる。

5$B_1$ と曲線数のトレードオフ(実測)
$p$ の桁数$B_1$成功までの曲線数ECMPollard $\rho$
8200020.22 s0.02 s
102000161.53 s0.27 s
122000282.96 s3.77 s
13100003110.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

自己評価

自分の回答

気づき・メモ