問題
$T$ 個の質問に答えよ。各質問では素数 $p$ と整数 $g,h$ が与えられる。
$$g^x\equiv h\pmod p$$
を満たす最小の非負整数 $x$ を出力せよ。存在しなければ -1 を出力せよ。
ただし、$p-1$ の素因数はすべて $10^5$ 以下であることが保証される。
入力形式
T
p_1 g_1 h_1
...
p_T g_T h_T
制約
| # | 条件 |
|---|---|
| 1 | $1\le T\le100$ |
| 2 | $2\le p\le10^{18}$、$p$ は素数 |
| 3 | $p-1$ の素因数はすべて $10^5$ 以下 |
| 4 | $1\le g,h<p$ |
| 5 | 入力はすべて整数 |
入出力例
入力例1
5
13 2 3
13 3 2
7 2 2
998244353 3 5
998244353 9 3出力例1
4
-1
1
109353319
-1($2^4=16\equiv3$。$3$ の冪は $3,9,1$ の繰り返しで $2$ は出ない。$7$ を法とする $2$ の位数は 3 なので、$x=1$ が最小。$998244353-1=2^{23}\cdot7\cdot17$ で、$9=3^2$ の冪は平方剰余だけを動き、$3$ は平方非剰余なので届かない)
背景と出典
Pohlig–Hellman は Pohlig と Hellman(1978)の離散対数アルゴリズム(私の記憶。原典は本セッションでは未確認)。群の位数が小さな素因数だけでできていると、離散対数は最大の素因数 $q$ について $O(\sqrt q)$ 程度で解ける。暗号で使う素数 $p$ に「$p-1$ が大きな素因数を持つ」こと(安全素数など)が求められるのは、この攻撃を避けるため、と理解している。
この問題の中核は 3 つ。
- $g$ の位数 $n$ を先に求める。$p-1$ の素因数 $q$ で、$g^{n/q}=1$ である限り $n$ を $q$ で割る
- 存在判定: $\mathbb{Z}_p^*$ は巡回群なので、$h\in\langle g\rangle\iff h^n=1$
- 最小解: 解は $\bmod n$ でただ 1 つに決まるので、$[0,n)$ の代表元が最小。$n$ を $p-1$ と取り違えると、$\bmod(p-1)$ の答えを出してしまう
前日の Day168-Q5 は合成数の法での拡張 BSGS($O(\sqrt m)$)だった。今回は法が素数で $10^{18}$ まで大きく、BSGS の $\sqrt p=10^9$ は不可能。代わりに $p-1$ の滑らかさを使う。
概念図
ヒント(クリックで段階的に開示)
ヒント1(方向性)
$n=\mathrm{ord}(g)=\prod q_i^{e_i}$ とする。$x\bmod q_i^{e_i}$ が全部わかれば CRT で $x\bmod n$ が決まる。$g_i=g^{n/q_i^{e_i}}$、$h_i=h^{n/q_i^{e_i}}$ とおくと、$g_i$ の位数は $q_i^{e_i}$ で、$g_i^{x}=h_i$。
ヒント2(アプローチ)
位数 $q^e$ の離散対数は、$x=d_0+d_1q+\dots+d_{e-1}q^{e-1}$($0\le d_k<q$)の桁ごとに解く。$\gamma=g_i^{q^{e-1}}$(位数 $q$)について、
$$\left(g_i^{-(d_0+\dots+d_{k-1}q^{k-1})}h_i\right)^{q^{e-1-k}}=\gamma^{d_k}$$
各桁は位数 $q\le10^5$ の離散対数なので、BSGS で $O(\sqrt q)$。
ヒント3(誘導)
order = p - 1
for q, e in fac: # p-1 の素因数分解
for _ in range(e):
if pow(g, order // q, p) == 1: order //= q
else: break
if pow(h, order, p) != 1: return -1
# order の各 q^e について桁ごとに d_k を BSGS で求め、CRT模範解答 (Python)
模範解答を表示する(まず自力で解いてから)
import sys
from math import isqrt
def sieve(n):
s = bytearray([1]) * (n + 1)
s[0] = s[1] = 0
for i in range(2, isqrt(n) + 1):
if s[i]:
s[i * i::i] = bytearray(len(range(i * i, n + 1, i)))
return [i for i in range(n + 1) if s[i]]
PRIMES = sieve(100000)
def bsgs(a, b, q, p):
# 位数 q の元 a について a^x = b (0 <= x < q) を解く。必ず解がある前提
m = isqrt(q) + 1
table = {}
e = 1
for j in range(m):
table.setdefault(e, j)
e = e * a % p
step = pow(a, -m, p) # a^{-m}
y = b
for i in range(m):
j = table.get(y)
if j is not None:
return i * m + j
y = y * step % p
raise ValueError("no log in subgroup")
def solve(p, g, h):
g %= p
h %= p
if p == 2:
return 0 # g = h = 1
# p - 1 を素因数分解(素因数はすべて 1e5 以下の保証)
n = p - 1
fac = []
for q in PRIMES:
if q * q > n and n > 1 and n <= 100000:
fac.append((n, 1))
n = 1
if n == 1:
break
if n % q == 0:
e = 0
while n % q == 0:
n //= q
e += 1
fac.append((q, e))
# g の位数 ord を p-1 から素因数を削って求める
order = p - 1
ofac = []
for q, e in fac:
for _ in range(e):
if pow(g, order // q, p) == 1:
order //= q
else:
break
cnt = 0
t = order
while t % q == 0:
t //= q
cnt += 1
if cnt:
ofac.append((q, cnt))
# 巡回群なので h が <g> に入る ⇔ h^ord = 1
if pow(h, order, p) != 1:
return -1
# Pohlig–Hellman: 各 q^e について x mod q^e を q 進の桁ごとに求め、CRT でまとめる
x_all, mod_all = 0, 1
for q, e in ofac:
qe = q ** e
g0 = pow(g, order // qe, p) # 位数 q^e
h0 = pow(h, order // qe, p)
gamma = pow(g0, q ** (e - 1), p) # 位数 q
ginv = pow(g0, -1, p)
x = 0
qk = 1
for k in range(e):
hk = pow(h0 * pow(ginv, x, p) % p, q ** (e - 1 - k), p)
d = bsgs(gamma, hk, q, p)
x += d * qk
qk *= q
# CRT: x_all mod mod_all と x mod qe を合わせる
t = (x - x_all) * pow(mod_all, -1, qe) % qe
x_all += mod_all * t
mod_all *= qe
return x_all % order if order > 1 else 0
def main():
data = sys.stdin.buffer.read().split()
t = int(data[0])
out = []
for i in range(t):
p, g, h = int(data[1 + 3 * i]), int(data[2 + 3 * i]), int(data[3 + 3 * i])
out.append(str(solve(p, g, h)))
print("\n".join(out))
main()
計算量: 素因数分解が $\pi(10^5)=9592$ 回の割り算、位数の計算が $O(\log p)$ 回の冪、本体が $\sum_i e_i(\sqrt{q_i}+\log p)$。$p\le10^{18}$ なら $\sum e_i\le60$、$\sqrt{q_i}\le317$。実測(Python 3.14.7、M1 Mac)で $T=100$、$p$ 最大 $5.7\times10^{17}$ が 0.08 s。
Step-by-Step 解説
$g$ が原始根なら位数は $p-1$。そうでないと位数は $p-1$ の真の約数 $n$ で、解は $\bmod n$ で決まる。位数を $p-1$ と決めつけて同じ計算をすると、部分群に解が無いと誤判定するか、$\bmod(p-1)$ の別の代表元を出す。$p<300$ の全 1581937 通りで、位数を $p-1$ と決めつけた版は 213432 通り誤答(最小でない 139717、解があるのに -1 が 73715、実測)。最小の例は $p=7,g=2,h=2$ で、正解 1 に対し 4 を出す。
$\mathbb{Z}_p^*$ は巡回群なので、位数 $n$ の部分群はただ 1 つで、それは「$y^n=1$ を満たす $y$ 全体」に等しい。$\langle g\rangle$ はその部分群なので、$h\in\langle g\rangle\iff h^n=1$。合成数の法では群が巡回とは限らないので、この判定は使えない(Day168-Q5 の拡張 BSGS が要る理由の 1 つ)。
$g_i=g^{n/q^e}$、$h_i=h^{n/q^e}$ とすると、$g^x=h$ から $g_i^x=h_i$。$g_i$ の位数は $q^e$ なので、これは $x\bmod q^e$ だけを決める。
$x\bmod q^e=d_0+d_1q+\dots$。既に求めた桁の分 $g_i^{-(\cdots)}$ を $h_i$ に掛けて消し、$q^{e-1-k}$ 乗すると、残りの高い桁が消えて $\gamma^{d_k}$ だけが残る($\gamma$ の位数が $q$ なので)。各桁は位数 $q$ の BSGS。$q=2,e=23$($998244353$)のような場合も 23 回の小さな探索で済む。
$x\bmod q_i^{e_i}$ を順に CRT で合わせると $x\bmod n$ が得られる。$g^x$ は $x\bmod n$ だけで決まるので、$[0,n)$ の代表元が最小の非負解。
迷った他案と採らなかった理由
| 案 | 計算量 | 採否 | 理由 |
|---|---|---|---|
| $x=0,1,\dots$ の反復 | $O(p)$ | 検算用のみ | $p<300$ の全 1581937 通りの照合に使った |
| BSGS を $\bmod p$ で直接 | $O(\sqrt p)$ | 検算用のみ | $p=10^{18}$ で $10^9$ 要素の表は不可。$p\le10^{10}$ の 100 件の照合に使った |
| Pohlig–Hellman(本解) | $O(\sum e_i\sqrt{q_i})$ | 採用 | 小素数で全件一致、大きい $p$ で 100/100 が $g^x=h$ かつ $x<$ 位数 |
| 各桁を BSGS でなく Pollard の ρ | $O(\sqrt q)$ 期待、メモリ $O(1)$ | 不採用 | $q\le10^5$ では表が 317 要素で済み、メモリの利点が無い。乱択で検算が面倒 |
| 指数計算法(index calculus) | 準指数 | 不採用 | $p-1$ が滑らかでない一般の $p$ 向け。本問の保証では過剰 |
前提が変わったら:
- $p-1$ に $10^{12}$ 級の素因数があるなら、その部分の BSGS が $10^6$ 要素で済むうちはまだ使える。$10^{18}$ 級なら Pohlig–Hellman は効かず、指数計算法の領域(本セッションでは扱わない)
- $p-1$ の素因数分解が与えられない一般の $p$ なら、Pollard の ρ(素因数分解)を先に入れる
- 法が合成数なら、群が巡回とは限らず、存在判定 $h^n=1$ が使えない。Day168-Q5 の拡張 BSGS か、法を素数冪に分けて別々に解く
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| 位数を $p-1$ と決めつける | $g$ を原始根だと思い込む | 位数を素因数で削って求める(213432/1581937 誤答) |
| 存在判定を省き、BSGS が見つけられないとき例外で落ちる | 部分群に解があると決めつける | 先に $h^n=1$ を確かめる |
| 桁ごとの補正で $g_i^{-x}$ ではなく $g^{-x}$ を掛ける | 射影前と後の元の取り違え | 射影後の $g_i$ の逆元 ginv |
| CRT の後に $\bmod n$ を取らない | CRT の結果は $[0,n)$ だが、$n=1$ の扱いを忘れる | x_all % order、order == 1 なら 0 |
| $p=2$ を特別扱いしない | $p-1=1$ で素因数が無い | $g=h=1$ なので 0 |
次のステップ
- 発展1: $p-1$ の素因数分解を Pollard の ρ で行い、「最大の素因数が $10^{10}$ 以下」まで広げる
- 発展2: 楕円曲線上の離散対数に Pohlig–Hellman を当て、曲線の位数が滑らかだと危険な理由を確かめる
- 発展3: Pohlig・Hellman(1978)の原典を読む(本セッションでは未確認)
検証ログ(本セッションで実行した数値)
- 反復との照合(実測): $p<300$ のすべての素数と、すべての $1\le g,h<p$ の全 1581937 通りで、$x=0,1,\dots,p-1$ の反復と一致(うち
-1は 652467 通り) - 大きい $p$(実測): $p-1$ が $10^5$ 以下の素因数だけになる素数を乱択で作り、$p\le10^{18}$(最大 566228048925458921)の 100 件で、出力 $x$ が $g^x\equiv h$ かつ $0\le x<$ 位数(位数は $p-1$ の全約数から最小のものを別経路で計算)を満たす。
-1の 18 件は $h^{\text{位数}}\ne1$ を確認 - BSGS との照合(実測): $p\le10^{10}$ の 100 件で、$\bmod p$ の素朴な BSGS(最小解)と全件一致
- 誤った方法(実測): 位数を $p-1$ と決めつけると 1581937 通り中 213432 通り誤答(最小でない 139717、
-1と誤る 73715、存在しないのに答えを出す 0) - 入力例(実測):
4-11109353319-1。位数を決めつけた版は 3 つ目で4を出す - 実行時間(実測): $T=100$、$p\le10^{18}$ で 0.08 s
- 扱っていない範囲: $p-1$ に $10^5$ を超える素因数がある入力(保証外。本解は誤った答えを出しうる)、Pollard の ρ・指数計算法、楕円曲線版、原典の確認