問題
$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 段。
- 因子基底: $B$ 以下の素数 $p_1,\dots,p_K$ を決める
- 関係式集め: ランダムな $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$ を求める
- 各クエリ: $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\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 解説
$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 本目からいきなり詰まる(実測)。
$g$ は原始根なので $g^q\equiv-1$。$h=g^x$ なら $h^q=(-1)^x$。だから $h^q\equiv1$ なら $x$ は偶数。偶奇を無視して $x\bmod q$ をそのまま出すと、1000 問中 492 問が誤答(実測。ほぼ半分)。
$r$ のすべての素因数が $B$ 以下 $\iff$ $r\mid P^E$。$P$(168 個の素数の積、約 1400 ビット)を $r$ で割った余りを $E$ 乗するだけで判定できる。168 個の素数で割り算を試すより速く、smooth だったときだけ分解する。
$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$ |
|---|---|---|---|---|
| 700 | 125 | 0.70 s | 1.83 s | 6.90 s |
| 1000 | 168 | 1.34 s | 1.99 s | 5.14 s |
| 1500 | 239 | 3.25 s | 3.89 s | 5.40 s |
$Q\le1000$ なら 700〜1000 が速い。$Q$ が 1 万を超えるなら 1000〜1500。本解は $Q$ の上限に合わせて 1000 に固定した。
$k$ を毎回乱数で引く代わりに $k,k+1,\dots$ と 1 ずつ増やして $g^k$ を掛け算 1 回で更新している。$g^k\bmod p$ は $k$ が連続でも smooth かどうかはほぼ独立に振る舞う、という経験則に頼っている(証明はしていない)。正しさには影響せず(どの $k$ でも関係式は正しい)、時間だけに関わる。
迷った他案と採らなかった理由
| 案 | 前計算 | 1 問あたり | 採否 | 理由 |
|---|---|---|---|---|
| 1 問ずつ BSGS | 0 | $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 の原典