Day 170-Q5 — 2026-10-05 | 指数計算法(index calculus)で多数の離散対数(安全素数 p=2q+1。小さい素数の対数を連立方程式で一度だけ求め、各クエリは「滑らかな数」を引くまで乱択)

2026-10-05 赤色 Master (Phase 8+) ★★★★★★★★★ 数論・代数(焦点帯外 M3)。Day168-Q5・Day169-Q5 の BSGS/Pohlig–Hellman は「1 問ずつ $O(\sqrt q)$」。同じ $p,g$ でクエリが 1000 個あると、BSGS の表を共有しても $p\approx10^{12}$ で表が数千万要素になる。小さい素数の対数を一度だけ求めておく指数計算法なら、前計算 1 回+クエリごとの乱択で済む

問題

$p=2q+1$($p,q$ はともに素数。安全素数)と、$p$ の原始根 $g$ が与えられる。$Q$ 個の整数 $h_1,\dots,h_Q$($1\le h_i\le p-1$)それぞれについて、

$$g^x\equiv h_i\pmod p,\qquad 0\le x\le p-2$$

を満たす $x$(原始根なので一意)を求めよ。

入力形式

p g Q
h_1 h_2 ... h_Q

制約

#条件
1$10^9\le p<10^{12}$、$p$ と $(p-1)/2$ はともに素数
2$g$ は $p$ の原始根
3$1\le Q\le1000$
4$1\le h_i\le p-1$
5入力はすべて整数

入出力例

入力例1

1000000007 5 5
1 5 1000000006 2 123456789

出力例1

0
1
500000003
381838282
981640996

($10^9+7$ は $(p-1)/2=500000003$ も素数の安全素数で、5 は原始根。$-1$ の対数は $(p-1)/2$。BSGS でも同じ値)

背景と出典

指数計算法は Western–Miller(1968)や Adleman(1979)にさかのぼる離散対数の準指数時間アルゴリズムで、数体ふるい法(NFS)の祖先にあたる、と私は理解している(原典は本セッションでは未確認)。考え方は 3 段。

  1. 因子基底: $B$ 以下の素数 $p_1,\dots,p_K$ を決める
  2. 関係式集め: ランダムな $k$ で $g^k\bmod p$ を計算し、それが $B$ 以下の素数だけで割り切れる($B$-smooth)なら、$g^k=\prod p_i^{e_i}$ から $k\equiv\sum e_i\log p_i\pmod{p-1}$。これを $K$ 本以上集めて連立方程式を解き、$\log p_i$ を求める
  3. 各クエリ: $h\cdot g^k$ が smooth になる $k$ を探し、$\log h\equiv\sum e_i\log p_i-k$

問題は 2 の連立方程式の法 $p-1$ が素数でないこと。$p-1$ が偶数なので、係数が偶数の行ではピボットの逆元が取れない。本問は安全素数に限ることで、

  • 法 $q$(素数)では体なので普通のガウスの消去法が使える
  • 法 2は解く必要すらない。$g$ が原始根なら「$x$ が偶数 $\iff h$ が平方剰余 $\iff h^q\equiv1$」(オイラーの規準)

と分解し、中国剰余定理で $x\bmod 2q$ に戻す。

概念図

前計算は 1 回(p と g だけで決まる)、クエリは 1 問ずつ乱択 ① 因子基底1000 以下の素数 168 個 ② 関係式集めg^k が smooth → Σ e_i L_i ≡ k ③ 法 q で消去L_i = log p_i mod q 法 p−1 で消去偶数係数で逆元なし ✗ 200 本中 69 本で詰まる ④ h·g^k が smooth になる kx ≡ Σ e_i L_i − k (mod q) ⑤ 偶奇はオイラーの規準h^q ≡ 1 ⇔ x が偶数 ⑥ CRT で x mod 2qxq の偶奇が合わなければ +q smooth 判定: P = 168 個の素数の積として、P^E mod r = 0 ⇔ r の素因数がすべて 1000 以下(E = p のビット長) 偶奇を捨てて xq をそのまま出すと 1000 問中 492 問誤答(実測)

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

ヒント1(方向性)

$p\approx10^{12}$ で 1 問ずつ BSGS をすると 1 問あたり約 $7\times10^5$ 歩($\sqrt q$)、1000 問で $7\times10^8$。表を共有して大きさ $m$ にしても、総コストは $m+Qp/m$ で、最適な $m=\sqrt{Qp}\approx3\times10^7$ 要素の辞書が要る。「どの $h$ にも共通の前計算」をもっと賢く作れないか。

ヒント2(アプローチ)

$\log$ は掛け算を足し算に変える。$h$ を小さい素数の積に分解できれば、小さい素数の $\log$ を足すだけで $\log h$ が出る。$h$ そのものが分解できなくても、$h\cdot g^k$ なら $k$ を変えてランダムに引き直せる。小さい素数の $\log$ は、$g^k$ が分解できた例を連立方程式にして求める。

ヒント3(誘導)
  • 因子基底: $B=1000$ 以下の素数 168 個
  • smooth 判定: $P=\prod p_i$ として、$r$ が smooth $\iff$ $P^{E}\equiv0\pmod r$($E$ は $p$ のビット長。$r$ の素因数の重複は $E$ 回まで)。割り算の試行より速い
  • 連立方程式は法 $q$ で、関係式を 1 本ずつ入れては既存のピボット行で消去する。ランクが $K$ になったら後退代入
  • クエリ: 偶奇は $h^q\bmod p$、法 $q$ の値は $h\cdot g^k$ を smooth にして $\sum e_iL_i-k$。最後に CRT

模範解答 (Python)

模範解答を表示する(まず自力で解いてから)
import sys
import random


def main():
    data = sys.stdin.buffer.read().split()
    p, g, qn = int(data[0]), int(data[1]), int(data[2])
    hs = [int(x) for x in data[3:3 + qn]]
    q = (p - 1) // 2                     # p = 2q + 1、q は素数
    rng = random.Random(12345)

    # 1. 因子基底: B 以下の素数
    B = 1000
    sieve = bytearray([1]) * (B + 1)
    sieve[0:2] = b"\x00\x00"
    for i in range(2, int(B ** 0.5) + 1):
        if sieve[i]:
            sieve[i * i::i] = bytearray(len(sieve[i * i::i]))
    fb = [i for i in range(2, B + 1) if sieve[i]]
    K = len(fb)
    P = 1
    for x in fb:
        P *= x
    E = p.bit_length()                   # r < p < 2^E なので、素因数の重複は E 回まで

    def factor(r):
        # r が B-smooth なら指数ベクトル(疎: {番号: 指数})、そうでなければ None
        if pow(P % r, E, r) != 0:
            return None
        vec = {}
        for idx, f in enumerate(fb):
            if r % f == 0:
                c = 0
                while r % f == 0:
                    r //= f
                    c += 1
                vec[idx] = c
                if r == 1:
                    break
        return vec

    # 2. 関係式を集め、F_q 上で消去する(行を 1 本ずつ入れて、ピボット行を保つ)
    piv = [None] * K                     # piv[c]: 列 c がピボットの行(係数リスト, 右辺)
    rank = 0
    k = rng.randrange(1, p - 1)
    r = pow(g, k, p)
    while rank < K:
        k += 1
        r = r * g % p                    # g^k を 1 回の掛け算で更新する
        vec = factor(r)
        if vec is None:
            continue
        row = [0] * K
        for idx, c in vec.items():
            row[idx] = c
        rhs = k % q
        for c in range(K):
            if row[c] == 0:
                continue
            if piv[c] is None:
                inv = pow(row[c], q - 2, q)
                row = [x * inv % q for x in row]
                rhs = rhs * inv % q
                piv[c] = (row, rhs)
                rank += 1
                break
            prow, prhs = piv[c]
            t = row[c]
            row = [(x - t * y) % q for x, y in zip(row, prow)]
            rhs = (rhs - t * prhs) % q
    # 後退代入で log(fb[c]) mod q を求める
    lg = [0] * K
    for c in range(K - 1, -1, -1):
        row, rhs = piv[c]
        s = rhs
        for d in range(c + 1, K):
            if row[d]:
                s -= row[d] * lg[d]
        lg[c] = s % q

    # 3. 各クエリ: 偶奇はオイラーの規準、mod q は h·g^k を smooth にして足し合わせる
    out = []
    for h in hs:
        par = 0 if pow(h, q, p) == 1 else 1
        k = rng.randrange(0, p - 1)
        r = h * pow(g, k, p) % p
        while True:
            vec = factor(r)
            if vec is not None:
                break
            k += 1
            r = r * g % p
        xq = (sum(c * lg[idx] for idx, c in vec.items()) - k) % q
        x = xq if xq % 2 == par else xq + q       # CRT: x ≡ xq (mod q), x ≡ par (mod 2)
        out.append(x)
    print("\n".join(map(str, out)))


main()

計算量: smooth になる確率を $\rho$ とすると、前計算は約 $K/\rho$ 回の smooth 判定+消去 $O(K^3)$、クエリは 1 問あたり約 $1/\rho$ 回の判定。$p\approx10^{12}$、$Q=1000$ で 1.37〜1.87 s、$p\approx1.4\times10^9$ で 1.04 s(Python 3.14.7、M1 Mac、実測)。乱択は時間だけに効き、答えは常に正しい(ラスベガス型。連立方程式はランクが $K$ になるまで集めるので、解は一意に決まる)。

Step-by-Step 解説

1法 $p-1$ を 2 と $q$ に分ける

$p-1=2q$ で $\gcd(2,q)=1$ なので、$x\bmod 2$ と $x\bmod q$ が分かれば CRT で $x\bmod p-1$ が決まる。法 $p-1$ のまま消去すると、係数が偶数の行でピボットの逆元が取れない。$p\approx9.8\times10^{11}$ で集めた最初の 200 本の関係式のうち 69 本は先頭の係数が $p-1$ と互いに素でなく、1 本目からいきなり詰まる(実測)。

2偶奇はオイラーの規準でただで分かる

$g$ は原始根なので $g^q\equiv-1$。$h=g^x$ なら $h^q=(-1)^x$。だから $h^q\equiv1$ なら $x$ は偶数。偶奇を無視して $x\bmod q$ をそのまま出すと、1000 問中 492 問が誤答(実測。ほぼ半分)。

3smooth 判定を 1 回の冪乗にする

$r$ のすべての素因数が $B$ 以下 $\iff$ $r\mid P^E$。$P$(168 個の素数の積、約 1400 ビット)を $r$ で割った余りを $E$ 乗するだけで判定できる。168 個の素数で割り算を試すより速く、smooth だったときだけ分解する。

4$B$ の選び方は実測で決める

$B$ を大きくすると smooth になりやすく、クエリが速くなるが、前計算の消去 $O(K^3)$ と関係式の本数 $K$ が増える。本セッションの実測($Q=1,1000$ は $p=920746362203$、$Q=10^4$ は別の $p=900857044343$):

$B$$K$$Q=1$$Q=1000$$Q=10^4$
7001250.70 s1.83 s6.90 s
10001681.34 s1.99 s5.14 s
15002393.25 s3.89 s5.40 s

$Q\le1000$ なら 700〜1000 が速い。$Q$ が 1 万を超えるなら 1000〜1500。本解は $Q$ の上限に合わせて 1000 に固定した。

5関係式に $g^k$ を連続で使ってよいか

$k$ を毎回乱数で引く代わりに $k,k+1,\dots$ と 1 ずつ増やして $g^k$ を掛け算 1 回で更新している。$g^k\bmod p$ は $k$ が連続でも smooth かどうかはほぼ独立に振る舞う、という経験則に頼っている(証明はしていない)。正しさには影響せず(どの $k$ でも関係式は正しい)、時間だけに関わる。

迷った他案と採らなかった理由

案前計算1 問あたり採否理由
1 問ずつ BSGS0$O(\sqrt q)$不採用$p\approx10^{12}$ で 1 問 $7\times10^5$ 歩 × 1000
表を共有する BSGS$m=\sqrt{Qp}$$p/m$不採用$p\approx1.2\times10^{10}$ で 3.16 s・373 MB(本解 1.23 s)。$p\approx10^{12}$ では表が約 $3\times10^7$ 要素で数 GB(導出)
Pollard の $\rho$(離散対数版)0$O(\sqrt q)$ 期待値不採用メモリは小さいが、1 問ずつの計算量は BSGS と同じ
法 $p-1$ のまま消去——不採用逆元が取れない(200 本中 69 本)
安全素数に限った指数計算法(本解)$K/\rho$ 回の判定+$O(K^3)$$1/\rho$ 回の判定採用5000 問すべて $g^x\equiv h$、BSGS と計 2000 問一致

前提が変わったら:

  • $p-1$ が小さい素因数だけなら Pohlig–Hellman(Day169-Q5)が速い。指数計算法の出番は $p-1$ に大きな素因数があるとき
  • $p-1=2q$ の形でないなら、$p-1$ の各素因数冪ごとに法を分けて消去する(素数冪の法では Hensel 持ち上げか、ピボットを単元に限る消去が要る)
  • $p$ が $10^{20}$ 以上なら smooth になる確率が急に下がり、因子基底を数千〜数万にして疎行列の解法(Lanczos・Wiedemann)が要る。さらに大きければ数体ふるい法
  • 楕円曲線上の離散対数には「小さい素数」に当たるものが無く、指数計算法は使えない(だから楕円曲線暗号は鍵が短い)

よくあるミス

ミス原因正しい書き方
法 $p-1$ でガウスの消去合成数の法でも体だと思う法 $q$ と法 2 に分ける(200 本中 69 本で逆元が無い)
$x\bmod q$ をそのまま出力偶奇の情報を捨てるオイラーの規準で偶奇を決めて CRT(492/1000 誤答)
関係式を $K$ 本ちょうど集めて解くランク落ちを考えないランクが $K$ になるまで集める
smooth 判定に試し割りだけを使う遅い$P^E\bmod r=0$ で判定してから分解
クエリで $h$ そのものだけ分解を試すsmooth でない $h$ は分解できない$h\cdot g^k$ を引き直す

次のステップ

  • 発展1: 安全素数でない $p$(例: $p-1=4q$)に広げ、法 4 の部分を $h^{q}$ の値から決める
  • 発展2: 関係式の行列は疎(1 行に数個の非零)。構造化ガウス消去で $K=2000$ まで広げ、$p\approx10^{15}$ で試す
  • 発展3: $L_p[1/2]$ の計算量の式から最適な $B$ を見積もり、本問の実測表と比べる

検証ログ(本セッションで実行した数値)

  • 正しさ(実測): 安全素数 5 つ(1437801227、11658547739、109779692447、863246504039、977043868523)で各 1000 問、計 5000 問すべて $0\le x<p-1$ かつ $g^x\equiv h$。各素数で $h=1$ → 0、$h=p-1$ → $(p-1)/2$、$h=g$ → 1 を含む
  • BSGS との照合(実測): $p=1437801227$ と $p=11658547739$ で各 1000 問、計 2000 問すべて一致
  • 誤った方法(実測): 偶奇を無視すると 1000 問中 492 問誤答。法 $p-1$ の消去は、集めた 200 本中 69 本で先頭の係数に逆元が無く、1 本目から詰まる
  • 実行時間(実測): $Q=1000$ で $p\approx1.4\times10^9$ 1.04 s、$1.2\times10^{10}$ 1.31 s、$1.1\times10^{11}$ 1.17 s、$8.6\times10^{11}$ 1.37 s、$9.8\times10^{11}$ 1.87 s。$g^k$ を毎回 pow で計算する初版は $B=1000$、$Q=1000$ で 2.69 s、$B=3000$ で 6.91 s(消去 $O(K^3)$ が支配的)
  • BSGS の比較(実測): 表を共有する BSGS は $p\approx1.4\times10^9$ で 0.94 s・177 MB、$p\approx1.2\times10^{10}$ で 3.16 s・373 MB
  • 入力例(実測): 0 1 500000003 381838282 981640996(BSGS でも同じ)
  • 扱っていない範囲: 安全素数でない $p$、$p\ge10^{12}$、疎行列の解法、連続する $k$ を使うことの理論的な裏付け、Western–Miller・Adleman の原典

自己評価

自分の回答

気づき・メモ