問題
$T$ 個のテストケースが与えられる。各ケースで整数 $a,b,m$ が与えられるので、
$$a^x\equiv b\pmod m$$
を満たす**最小の非負整数 $x$** を求めよ。存在しなければ $-1$ を出力せよ。ただし $a^0=1$($a=0$ のときも)とし、$m=1$ のときは常に $x=0$ とする。$a,b$ は $m$ で割った余りで考えてよい。
入力形式
T
a_1 b_1 m_1
...
a_T b_T m_T
制約
| # | 条件 |
|---|---|
| 1 | $1\le T\le100$ |
| 2 | $1\le m\le10^9$ |
| 3 | $0\le a,b\le10^9$ |
| 4 | 入力はすべて整数。$\gcd(a,m)=1$ とは限らない |
入出力例
入力例1
4
2 8 12
2 3 7
4 0 8
6 1 9出力例1
3
-1
2
0($2^3=8$。$2^x\bmod7\in\{1,2,4\}$ なので 3 は作れない。$4^2=16\equiv0\pmod 8$。$6^0=1$)
入力例2
3
2 4 8
10 1 17
3 5 7出力例2
2
0
5($2^2=4$。$10^0=1$。$3^5=243\equiv5\pmod 7$)
背景と出典
BSGS は Shanks(1971)による離散対数の $O(\sqrt m)$ 解法と私は記憶している(未確認)。「$\gcd(a,m)\ne1$ でも使えるようにする拡張」は、競技プログラミング界で「拡張 BSGS」の名で広まった手法で、原典は特定できない(私の把握では中国のコミュニティ起源。未確認)。
この問題の核は「$a$ が $m$ と互いに素でないと、$a$ は $\bmod m$ で逆元を持たず、$a^{-s}$ を掛ける giant-step が使えない」ことへの対処。**$\gcd(a,m)=g$ を方程式から割り出し**、$m$ を $m/g$ に縮めて、互いに素になるまで繰り返す。割るたびに左辺の係数が増え、$x$ が 1 ずつ減る。
概念図
ヒント(クリックで段階的に開示)
ヒント1(方向性)
$\gcd(a,m)=1$ なら、$x=si-j$($0\le j<s$、$s\approx\sqrt m$)とおいて $a^{si}\equiv b\,a^{j}$ と変形できる。右辺 $b\,a^j$ をすべて表に覚え(baby-step)、左辺 $a^{si}$ を $i=1,2,\dots$ で順に作って表と突き合わせる(giant-step)。
ヒント2(アプローチ)
$\gcd(a,m)=g>1$ のとき、$a^x\equiv b\pmod m$ が解を持つには $g\mid b$ が要る($g\mid m$ なので $a^x\equiv b$ の両辺を $g$ で割れる)。割ると $(a/g)\,a^{x-1}\equiv b/g\pmod{m/g}$。これを、$\gcd(a,m')=1$ になるまで繰り返す。左辺に残る係数 $k=\prod(a/g_t)$ と、割った回数 $\mathrm{cnt}$ を覚える。
ヒント3(誘導)
互いに素になったら $k\cdot a^{x'}\equiv b'\pmod{m'}$ を BSGS で解く($x=x'+\mathrm{cnt}$)。ただし割る途中で $k\equiv b'$ になるなら、その時点の $\mathrm{cnt}$ が答えの候補(小さい $x$ はこのループで拾う)。baby-step は $x'\ge0$ を、giant-step は $i\ge1$ から始めるので、$x'=0$($k\equiv b'$)は別に判定する。
模範解答 (Python)
模範解答を表示する(まず自力で解いてから)
import sys
from math import gcd, isqrt
def discrete_log(a, b, m):
# 最小の x >= 0 で a^x ≡ b (mod m)。無ければ -1
a %= m
b %= m
if m == 1:
return 0
k = 1 % m
cnt = 0
while True:
if k == b:
return cnt # a^cnt ≡ b を、gcd を割る途中でも検出する
g = gcd(a, m)
if g == 1:
break
if b % g:
return -1
b //= g
m //= g
k = k * (a // g) % m
cnt += 1
# ここから gcd(a, m) = 1。k * a^x ≡ b (mod m) を解く(baby-step giant-step)
s = isqrt(m) + 1
table = {}
cur = b
for j in range(s): # b * a^j を覚える(同じ値は大きい j を残す)
table[cur] = j
cur = cur * a % m
step = pow(a, s, m)
cur = k
for i in range(1, s + 1):
cur = cur * step % m # k * a^(s i)
if cur in table:
x = i * s - table[cur]
if x >= 0:
return x + cnt
return -1
def main():
data = sys.stdin.buffer.read().split()
t = int(data[0])
out = []
for i in range(t):
a, b, m = int(data[1 + 3 * i]), int(data[2 + 3 * i]), int(data[3 + 3 * i])
out.append(discrete_log(a, b, m))
print("\n".join(map(str, out)))
if __name__ == "__main__":
main()
計算量: 1 ケース $O(\sqrt m)$($m=10^9$ で辞書に約 3.2 万要素)。実測(Python 3.14.7、M1 Mac)で $T=100$、$m\approx10^8$〜$10^9$ が 0.5 s、$\gcd\ne1$ の大きい例 100 件が 0.1〜0.15 s。
Step-by-Step 解説
$s=\lceil\sqrt m\rceil$ とし、$x=si-j$ と書く。$k\cdot a^{si-j}\equiv b$ を $a^j$ 倍して $k\,a^{si}\equiv b\,a^j$。右辺 $b\,a^j$($j=0..s-1$)を辞書に入れ(同じ値なら大きい $j$ に上書きして、$x=si-j$ を小さくする)、左辺を $i=1..s$ で順に作る。最初に一致した $i$ で $x=si-j$ が最小。$i=0$ から始めないのは $x=-j\le0$ を避けるため。
$a^x\equiv b\pmod m$ で $g=\gcd(a,m)>1$、$x\ge1$ なら $g\mid a^x$、$g\mid m$ から $g\mid b$ が必要。満たさなければ、$x\ge1$ の解は無い。両辺と $m$ を $g$ で割ると $(a/g)\,a^{x-1}\equiv b/g\pmod{m/g}$。これを繰り返し、係数 $k=\prod(a/g_t)$ を積み、$\mathrm{cnt}$ を数える。$m$ が縮むたびに $\gcd(a,m)$ は真に減る($m$ の素因数のうち $a$ と共通のものが 1 つ以上消えるため、概算で高々 30 回)。
割る前の $a^{\mathrm{cnt}}\equiv b$ が成立する小さい $x$($x<\mathrm{cnt}$)は、BSGS の探索対象 $x'\ge0$ の外になる。そこでループの先頭で $k\equiv b$(現在の $m$ で)を毎回調べ、成り立てば $\mathrm{cnt}$ を返す。この判定を落とすと、$m\le120$ の全数検査で 583220 通り中 4254 通りが誤る(実測)。
教科書通りの BSGS($i=1..s$、$x=0$ の判定なし)は、$\gcd(a,m)=1$ の 351177 通りのうち 4385 通りで誤り、**そのすべてが答え $x=0$($b\equiv1$)を見逃していた**(実測)。$\gcd\ne1$ を含めると 583220 通り中 96416 通りで誤る。
最小の $x$ を答えることは、全数検査($a,b<m+3$ の全組合せを反復で解いた最小値と比較)で確かめた。大きい $m$ では、返った $x$ が $a^x\equiv b$ を満たすこと(100 件)と、手前に解が無いこと(20 件の抜き取りで反復して確認)を実測した。
迷った他案と採らなかった理由
| 案 | 計算量 | 採否 | 理由 |
|---|---|---|---|
| $x=0,1,2,\dots$ と反復 | $O(\text{周期})\le O(m)$ | 検算用のみ | $m\le3000$ なら一瞬。$m=10^9$ は不可 |
| 教科書版 BSGS($\gcd=1$ 前提) | $O(\sqrt m)$ | 不採用 | $\gcd\ne1$ で 96416/583220 誤答(実測)。互いに素でも $x=0$ を見逃す |
| 拡張 BSGS(本解) | $O(\sqrt m+\log m)$ | 採用 | 全数 627860 通りが反復と一致 |
| Pohlig–Hellman(群の位数が滑らかな場合) | $O(\sum\sqrt{p_i^{e_i}})$ | 不採用 | 位数の素因数分解が要る。$m$ が一般だと準備が重い。$m$ が素数で $m-1$ が滑らかなら強い |
| Pollard の $\rho$(離散対数版) | 期待 $O(\sqrt m)$、メモリ $O(1)$ | 不採用 | 乱択で最小の $x$ が取れない。周期に注意が要る |
前提が変わったら:
- **$m$ が $10^{12}$ を超える**なら辞書が $10^6$ を超える。メモリを節約するなら $\rho$ 法(最小解でなくてよい場合)
- **$m$ が素数で位数が滑らか**なら Pohlig–Hellman の方が速い(暗号の文脈では滑らかな位数は避ける。ここは競技の設定で別の話)
- **複数の質問が同じ $a,m$ で $b$ だけ違う**なら、baby-step の表を使い回して giant-step を $b$ ごとに回す、という分け方がある(本問は $T\le100$ で個別に解く)
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| $\gcd(a,m)=1$ を仮定する | 教科書の BSGS をそのまま写す | $\gcd$ を割るループを前置(96416/583220 で誤答) |
| 割る途中の $k\equiv b$ を見ない | ループ終了後の BSGS だけで十分と思う | ループ先頭で毎回判定(4254/583220 で誤答) |
| $x=0$($b\equiv1$)を見逃す | giant-step を $i=1$ から始めている | $k\equiv b$ の判定が $x'=0$ を担当(互いに素の 4385/351177 が全部これ) |
| baby-step の辞書を最小の $j$ で残す | 「先に見たもの優先」と思う | 大きい $j$ で上書き($x=si-j$ が小さくなる向き) |
| $b\ge m$ のまま比較する | 入力を $m$ で割らない | 最初に b %= m |
次のステップ
- 発展1: 位数($a^x\equiv1$ の最小の $x\ge1$)を BSGS で求める($b=1$ で $x=0$ を除く)
- 発展2: 同じ $a,m$ で $b$ が多数のとき、baby-step の表を共有する
- 発展3: Pohlig–Hellman を実装し、$m-1$ が滑らかな素数 $m$ で BSGS と速度を比べる(未実施)
検証ログ(本セッションで実行した数値)
- 全数検査(実測): $m=1..120$、$a,b=0..m+2$($m$ 以上の入力も含む)の全 627860 通りで、反復で最小の $x$ を求めた結果と不一致 0 件
- 誤った方法(実測、$m\le120$、$a,b<m$ の全 583220 通り): 教科書版 BSGS が 96416 通りで誤答($\gcd(a,m)=1$ の 351177 通りに限ると 4385 通りで、その 4385 通りはすべて答えが $x=0$)。割る途中の $k\equiv b$ 判定を省いた版は 4254 通りで誤答
- 無作為検査(実測): $m\le3000$、$a,b\le3m$ の 3000 件で反復と不一致 0 件
- 大きい法(実測): $T=100$、$m\in[10^8,10^9]$、$b=a^x\bmod m$($x\le10^6$)で、返った $x$ が $a^x\equiv b$ を満たすこと 100/100。うち 20 件で、返った $x$ より手前に解が無いことを反復で確認(20/20)。$\gcd\ne1$ の例 100 件($m=g\cdot[10^7,10^8]$)も 100/100
- 入力例(実測): 例 1
3 -1 2 0、例 22 0 5 - 実行時間(実測): $T=100$、$m\approx10^9$ が 0.5 s、$\gcd\ne1$ の 100 件が 0.1〜0.15 s
- 扱っていない範囲: $m>10^9$、$\gcd$ を割る回数の上界の証明、Pohlig–Hellman・$\rho$ 法との比較、原典(Shanks 1971 など)の確認、大きい $m$ での「手前に解が無い」の全件確認(20 件の抜き取りのみ)