問題
素数 $p$ と、$p$ の原始根 $g$ が与えられる。$T$ 個の整数 $h_1,\dots,h_T$ それぞれについて、$g^x\equiv h_j\pmod p$ を満たす $0\le x<p-1$ を求めよ($g$ は原始根なのでちょうど 1 つ存在する)。
入力形式
p g T
h_1 h_2 ... h_T
制約
| # | 条件 |
|---|---|
| 1 | $10^4<p<10^{12}$、$p$ は素数、$g$ は $p$ の原始根 |
| 2 | $p-1=s\cdot q$ と書ける。$s$ の素因数はすべて $10^4$ 以下、$q$ は $10^4$ より大きい素数で、$q\nmid s$ |
| 3 | $1\le T\le100$、$1\le h_j<p$ |
入出力例
入力例1
60223 3 3
1 3 2出力例1
0
1
11438($p-1=60222=2\cdot3\cdot10037$)
入力例2
1264663 3 2
2 1000000出力例2
440092
969474($p-1=2\cdot3^2\cdot7\cdot10037$。$3^2$ があるので、Pohlig–Hellman は法 3 ではなく法 9 まで求める)
入力例3
943367993339 2 1
123456789出力例3
793341950834($p-1=2\cdot471683996669$。3 例とも $g^x\bmod p$ を計算して $h$ に戻ることを確認)
背景と出典
- Pohlig–Hellman(Day169-Q5): 位数 $p-1=\prod r^a$ の巡回群の離散対数は、各 $r^a$ の部分群での離散対数に分かれ、さらに $r$ 進の 1 桁ずつ $O(\sqrt r)$ で求まる。$r$ が小さければ速い
- 指数計算法(Day170-Q5): 小さい素数の集合(因子基底)の対数を、「$g^e\bmod p$ が因子基底だけで割り切れる(smooth)」関係式を集めて連立方程式で求める。大きい素数 $q$ の法で解けば、$x\bmod q$ が $O(\sqrt q)$ より速く出る
本問はこの 2 つを組み合わせる。$x\bmod q$ は指数計算法で、$x\bmod r^a$($r^a\| s$)は Pohlig–Hellman で求め、中国剰余定理で $x\bmod(p-1)$ に戻す。指数計算法の考え方は Adleman(1979)などに、Pohlig–Hellman は Pohlig–Hellman(1978)にさかのぼる、と私は記憶している(どちらの原典も本セッションでは確認していない)。
連立方程式を法 $p-1$ のまま解かないのは、$p-1$ が合成数でピボットの逆元が無いことがあるから。法 $q$(素数)なら体なので掃き出しがそのまま使える。本問は自作。
概念図
ヒント(クリックで段階的に開示)
ヒント1(方向性)
$x\bmod(p-1)$ が欲しい。$p-1=s\cdot q$ で $\gcd(s,q)=1$ なので、$x\bmod s$ と $x\bmod q$ を別々に求めて CRT で合わせればよい。$x\bmod s$ はさらに $s$ の素数冪ごとに分ける。
ヒント2(アプローチ)
- 小さい素数冪 $r^a$: $g_r=g^{(p-1)/r}$(位数 $r$)を使い、$x=d_0+d_1r+\dots$ の桁 $d_j$ を $\bigl(h\,g^{-x_{<j}}\bigr)^{(p-1)/r^{j+1}}=g_r^{d_j}$ から BSGS で 1 桁ずつ求める
- 大きい素数 $q$: 因子基底 $\{2,3,5,\dots\le B\}$ の対数を法 $q$ で求める。$g^e\bmod p=\prod b_i^{c_i}$ なら $e\equiv\sum c_i\log b_i\pmod{p-1}$、したがって法 $q$ でも成り立つ
ヒント3(誘導)
- 関係式は因子基底の個数 $k$ より 20 本多く集め、法 $q$ で掃き出す。自由変数に依存しない行の変数だけを「分かった対数」とする
- 各 $h$ について、$h\cdot g^{t}\bmod p$ が smooth で、出てくる素数の対数が全部分かっている $t$ をランダムに探す。$\log h\equiv\sum c_i\log b_i-t\pmod q$
- smooth の判定は「$n\mid P^{40}$($P$ は因子基底の積、$n<2^{40}$)」を
pow(P, 40, n) == 0で 1 回の冪乗で済ませる
模範解答 (Python)
模範解答を表示する(まず自力で解いてから)
import sys
import random
from math import isqrt
def small_primes(b):
s = bytearray([1]) * (b + 1)
s[0] = s[1] = 0
for i in range(2, isqrt(b) + 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 factor_small(n, ps, prod):
# n が ps だけで割り切れる(B-smooth)なら指数の辞書、そうでなければ None
if pow(prod, 40, n) != 0: # n < 2^40 なので、smooth ⇔ n | prod^40
return None
e = {}
for i, q in enumerate(ps):
if n % q == 0:
c = 0
while n % q == 0:
n //= q
c += 1
e[i] = c
if n == 1:
return e
return e if n == 1 else None
def bsgs(g, h, order, p):
# g の位数が order の部分群で g^x = h を解く(x in [0, order))
m = isqrt(order) + 1
table = {}
cur = 1
for j in range(m):
table.setdefault(cur, j)
cur = cur * g % p
step = pow(g, (p - 2) * m, p) # g^{-m}
cur = h
for i in range(m + 1):
if cur in table:
return (i * m + table[cur]) % order
cur = cur * step % p
return None
def solve_mod_q(rows, rhs, k, q):
# rows[r][i] x_i = rhs[r] (mod q) を掃き出し、決まった変数だけ返す
a = [row[:] + [r] for row, r in zip(rows, rhs)]
where = [-1] * k
r = 0
for c in range(k):
piv = next((i for i in range(r, len(a)) if a[i][c] % q), None)
if piv is None:
continue
a[r], a[piv] = a[piv], a[r]
inv = pow(a[r][c], q - 2, q)
a[r] = [x * inv % q for x in a[r]]
for i in range(len(a)):
if i != r and a[i][c] % q:
f = a[i][c]
ai, ar = a[i], a[r]
a[i] = [(x - f * y) % q for x, y in zip(ai, ar)]
where[c] = r
r += 1
sol = {}
for c in range(k):
if where[c] >= 0:
row = a[where[c]]
if all(row[j] % q == 0 for j in range(k) if j != c): # 自由変数に依存しない行だけ使う
sol[c] = row[k] % q
return sol
def main():
data = sys.stdin.read().split()
p, g, t = int(data[0]), int(data[1]), int(data[2])
hs = [int(x) for x in data[3:3 + t]]
rng = random.Random(12345)
n = p - 1
# p-1 = (小さい素因数の積) × q(q は 10^4 より大きい素数、高々 1 個)
fac = {}
m = n
for r in small_primes(10 ** 4):
while m % r == 0:
fac[r] = fac.get(r, 0) + 1
m //= r
q = m if m > 1 else None
# ---- 大きい素数 q の成分: 指数計算法で log mod q ----
if q is not None:
B = 1000
ps = small_primes(B)
k = len(ps)
prod = 1
for r in ps:
prod *= r
rows, rhs = [], []
while len(rows) < k + 20:
e = rng.randrange(1, n)
ex = factor_small(pow(g, e, p), ps, prod)
if ex is not None:
row = [0] * k
for i, c in ex.items():
row[i] = c
rows.append(row)
rhs.append(e % q)
logq = solve_mod_q(rows, rhs, k, q)
out = []
for h in hs:
res, mods = [], []
# 小さい素数冪 r^a の成分: Pohlig–Hellman(桁ごとに BSGS)
for r, a in fac.items():
gr = pow(g, n // r, p) # 位数 r の元
x = 0
ra = 1
for j in range(a):
hj = pow(h * pow(g, n - x, p) % p, n // (ra * r), p)
d = bsgs(gr, hj, r, p)
x += d * ra
ra *= r
res.append(x)
mods.append(r ** a)
if q is not None:
while True: # h * g^s が B-smooth で、出てくる素数の log が全部分かるまで
s = rng.randrange(n)
ex = factor_small(h * pow(g, s, p) % p, ps, prod)
if ex is not None and all(i in logq for i in ex):
res.append((sum(c * logq[i] for i, c in ex.items()) - s) % q)
mods.append(q)
break
x, md = 0, 1 # CRT
for r_, m_ in zip(res, mods):
t_ = (r_ - x) * pow(md, -1, m_) % m_
x += md * t_
md *= m_
out.append(x % n)
print('\n'.join(map(str, out)))
if __name__ == "__main__":
main()
計算量: 指数計算法の関係式集め(因子基底 $k=168$、$p\approx10^{12}$ で smooth の確率は $u=\log p/\log B\approx4$ の Dickman 関数 $\rho(4)\approx0.005$ 程度と記憶)と、$k\times(k+20)$ の掃き出し $O(k^3)$。クエリごとに smooth な $h g^t$ を探す試行(期待 $1/\rho$ 回)と、Pohlig–Hellman の $\sum a\sqrt r$。$p\approx9.4\times10^{11}$、$T=100$ で 0.70〜1.26 s(Python 3.14.7、M1 MacBook Pro、負荷平均 18〜22 の混んだ時間帯での実測、起動込み)。
Step-by-Step 解説
$10^4$ 以下の素数で割れるだけ割り、残りが $q$(制約により素数か 1)。$q$ は $\sqrt{q}$ が最大 $7\times10^5$ 程度になり、BSGS ではクエリ 1 個あたり約 0.43 s・$T=100$ で約 44 s(表の作成 0.70 s + 0.43 s×100 の見積もり。1 クエリぶんは実測)。
$r^a\| (p-1)$ について、$x\bmod r^a$ を $r$ 進で 1 桁ずつ求める。$j$ 桁目までの値 $x_{<j}$ が分かっているとき、$(h\,g^{-x_{<j}})^{(p-1)/r^{j+1}}$ は位数 $r$ の部分群に入り、$g_r^{d_j}$ に等しい。$r\le10^4$ なので BSGS は $\sqrt r\le100$ 歩。$a\ge2$ を忘れて $x\bmod r$ だけ求めると、CRT の法が足りずに誤る(400 クエリ中 53 個、実測)。
ランダムな $e$ で $g^e\bmod p$ を作り、因子基底だけで割り切れるものを集める。1 本の関係式は $e\equiv\sum c_i\log b_i\pmod{p-1}$。これを法 $q$ に落とすと、法が素数なので掃き出しができる。法 $p-1$ のままだと、ピボットが $p-1$ と互いに素でないとき逆元が無い。
関係式に一度も出てこない素数(大きめの素数)は、対数が決まらない。掃き出し後、「その変数のピボット行が他の自由変数に依存しない」ものだけを分かった対数とする。クエリで smooth な値が見つかっても、分からない素数を含むなら捨てて別の $t$ を試す。
$h\,g^t\bmod p$ が smooth で $\prod b_i^{c_i}$ なら、$\log h+t\equiv\sum c_i\log b_i\pmod q$。
$x\bmod r^a$(各素数冪)と $x\bmod q$ を順に合わせる。法は互いに素なので、pow(md, -1, m_) で逆元が取れる。
迷った他案と採らなかった理由
| 案 | 計算量 | 採否 | 理由 |
|---|---|---|---|
| 全体で BSGS | $O(\sqrt p)$/クエリ | 不採用 | $p\approx10^{12}$ で表 0.70 s+1 クエリ 0.43 s(実測)、$T=100$ で約 44 s(見積もり) |
| 法 $q$ 成分だけ Pollard の $\rho$(離散対数版) | $O(\sqrt q)$/クエリ | 不採用 | メモリは小さいが計算量は BSGS と同じ。$T=100$ では関係式を共有できる指数計算法が有利 |
| 指数計算法の連立方程式を法 $p-1$ のまま解く | $O(k^3)$ | 不採用 | 合成数の法ではピボットに逆元が無いことがある。法を素因数ごとに分ける必要があり、小さい素因数側は Pohlig–Hellman の方が単純 |
| Day170-Q5 の方法(法 2 と法 $q$ だけ) | 同上 | 不採用(誤り) | $p=2q+1$ でない限り $s$ の他の素因数を落とす |
| 法 $q$ は指数計算法、素数冪は Pohlig–Hellman、CRT(本解) | 上記 | 採用 | 0.70〜1.26 s |
前提が変わったら:
- $p-1$ に大きい素因数が 2 つ以上ある(例 $p-1=2q_1q_2$)なら、関係式は同じものを使い回し、法 $q_1$ と法 $q_2$ でそれぞれ掃き出せばよい。$p-1$ の分解自体に Pollard の $\rho$ が要る
- $p>2^{40}$ なら、smooth 判定の指数 40 を $\lceil\log_2p\rceil$ に上げる。$p\approx10^{18}$ では $u=\log p/\log B$ が大きくなり、$B$ を数万に増やさないと関係式が集まらない(本セッションでは未計測)
- $g$ が原始根でないなら、解が無い場合や複数ある場合がある。$g$ の位数を求め、その約数ごとに Pohlig–Hellman をし直す
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| 素数冪 $r^a$ を法 $r$ だけで解く | $p-1$ を素因数の集合として扱い、指数を忘れる | 400 クエリ中 53 個で誤答(実測、$s$ に平方因子を持つ $p$ は 40 個中 10 個)。$r$ 進で $a$ 桁求める |
| 連立方程式を法 $p-1$ で掃き出す | 合成数の法で逆元を取る | 法 $q$(素数)で掃き出す |
| 決まらない変数の値を 0 として使う | 関係式に出てこない素数がある | 自由変数に依存しない行の変数だけを使い、未知の素数を含む $h g^t$ は捨てる |
| smooth 判定で毎回 168 個の素数で試し割り | 大半は smooth でないので無駄 | pow(P, 40, n) == 0 で 1 回の冪乗に |
| CRT の前に $x\bmod q$ を負のまま使う | Python の % は非負だが、他言語では負になる | 最後に % (p - 1) |
次のステップ
- 発展1: $p-1$ に大きい素因数が 2 つ以上ある場合(関係式を共有して法ごとに掃き出す)
- 発展2: $p\approx10^{18}$ に広げる(因子基底を大きくし、掃き出しを疎行列向けの Lanczos/Wiedemann にする)
- 発展3: Pohlig–Hellman(1978)と指数計算法の原典を確認する(本セッションでは未確認)
検証ログ(本セッションで実行した数値)
- 正解との照合(実測): $10^6\le p<10^{12}$ で $p-1=s\cdot q$($s$ は $10^4$ 以下の素因数を 1〜5 個、$q>10^4$)のランダムな素数 60 個 × 各 20 クエリ(計 1200 個)で、乱数で選んだ $x$ から $h=g^x$ を作り、出力が $x$ に一致。不一致 0、1 回の実行は最大 0.92〜1.46 s
- 誤った方法(実測): Pohlig–Hellman を法 $r$ だけで解く版は、40 個の素数 × 10 クエリの 400 個中 53 個で誤答($s$ に平方因子を持つ素数は 40 個中 10 個)
- 入力例(実測): 例 1
0 1 11438、例 2440092 969474、例 3793341950834。いずれも $g^x\bmod p=h$ を確認 - 実行時間(実測、起動込み、負荷平均 18〜22): $p\approx9.4\times10^{11}$、$T=100$ で $s=2$: 0.80〜1.26 s、$s=210$: 0.86〜1.01 s、$s=7272$: 0.70〜0.77 s
- 比較(実測+見積もり): 全体の BSGS は $p=943367993339$ で表の作成 0.70 s、1 クエリ平均 0.43 s(5 クエリ)。$T=100$ なら約 44 s(見積もり)
- 扱っていない範囲: 大きい素因数が 2 つ以上の $p-1$、$p>2^{40}$、原始根でない $g$、原典の確認