Day 088-Q1 — Pohlig-Hellman法(離散対数問題・$p-1$ の滑らかさを利用)

2026-07-11 赤色 Master / Phase 8+ ★★★★★★★★★ Pohlig-Hellman・BSGS・CRT

問題

素数 $p$ と、$\bmod p$ における原始根 $g$、および整数 $y$($1 \le y \le p-1$)が与えられる。$g^x \equiv y \pmod p$ を満たす最小の非負整数 $x$($0 \le x \le p-2$)を求めよ。$p-1$ の素因数はすべて $10^6$ 以下であることが保証される。

制約

パラメータ範囲備考
$p$$3 \le p \le 10^{18}$素数
$g$$\bmod p$ の原始根
$y$$1 \le y \le p-1$離散対数の対象
$p-1$ の素因数すべて $\le 10^6$滑らかな数(B-smooth)

入出力例

入力例1

23 5 17

出力例1

7

入力例2

41 6 1

出力例2

0

例1: $5^7 \bmod 23 = 17$、$p-1=22=2\times11$。例2: $g^0=1$ が最小解。

概念図: $p-1$ を素因数分解して部分群に分割し CRT で統合

位数 p-1 = q1^e1 × q2^e2 × ... の各成分ごとに離散対数を解く 群全体: 位数 p-1(大きい) → 直接 BSGS だと O(√(p-1)) で遅い 部分群 位数 q1^e1 x mod q1^e1 をBSGSで解く 部分群 位数 q2^e2 x mod q2^e2 をBSGSで解く 部分群 位数 q3^e3 ... 同様に解く CRT で x mod (p-1) を復元

ヒント

ヒント1(方向性)

単純な BSGS は $O(\sqrt p)$。$p\le10^{18}$ では $\sqrt p\approx10^9$ で間に合わない。$p-1$ の素因数が小さいという特別な構造を活かせないか考える。

ヒント2(アプローチ)

$p-1=\prod q_i^{e_i}$($q_i\le10^6$)と分解し、中国剰余定理より $x\bmod(p-1)$ を求めることは各素数冪ごとに $x\bmod q_i^{e_i}$ を求めて統合することと同値。各成分は位数 $q_i^{e_i}$ の部分群上の離散対数に帰着でき、BSGS が $O(\sqrt{q_i})$ で高速に効く。

ヒント3(ほぼ答え)
def pohlig_hellman_prime_power(g, y, q, e, p):
    gamma = pow(g, q ** (e - 1), p)  # 位数 q の元
    x = 0
    g_inv = pow(g, -1, p)
    for k in range(e):
        exp = q ** (e - 1 - k)
        yk = pow((y * pow(g_inv, x, p)) % p, exp, p)
        xk = bsgs_small(gamma, yk, p, q)
        x += xk * (q ** k)
    return x

模範解答

import sys
import math

def factorize(n):
    factors = {}
    d = 2
    while d * d <= n:
        while n % d == 0:
            factors[d] = factors.get(d, 0) + 1
            n //= d
        d += 1
    if n > 1:
        factors[n] = factors.get(n, 0) + 1
    return factors

def bsgs_small(g, h, p, order):
    m = math.isqrt(order) + 1
    table = {}
    e = 1
    for j in range(m):
        table.setdefault(e, j)
        e = e * g % p
    ginv_m = pow(g, -m, p)
    cur = h % p
    for i in range(m):
        if cur in table:
            x = i * m + table[cur]
            if x < order:
                return x
        cur = cur * ginv_m % p
    return -1

def pohlig_hellman_prime_power(g, y, q, e, p):
    gamma = pow(g, q ** (e - 1), p)
    x = 0
    g_inv = pow(g, -1, p)
    for k in range(e):
        exp = q ** (e - 1 - k)
        yk = pow((y * pow(g_inv, x, p)) % p, exp, p)
        xk = bsgs_small(gamma, yk, p, q)
        x += xk * (q ** k)
    return x

def crt_combine(residues, moduli):
    x, m = 0, 1
    for r, mi in zip(residues, moduli):
        t = ((r - x) * pow(m, -1, mi)) % mi
        x = x + m * t
        m *= mi
    return x % m

def solve():
    p, g, y = map(int, sys.stdin.readline().split())
    n = p - 1
    factors = factorize(n)
    residues, moduli = [], []
    for q, e in factors.items():
        qe = q ** e
        gi = pow(g, n // qe, p)
        yi = pow(y, n // qe, p)
        xi = pohlig_hellman_prime_power(gi, yi, q, e, p)
        residues.append(xi)
        moduli.append(qe)
    x = crt_combine(residues, moduli)
    print(x)

solve()

計算量: 因数分解 $O(B)$($B=10^6$)。各成分 $O(e_i\sqrt{q_i})$。全体で単純 BSGS の $O(\sqrt p)$ より大幅高速。

Step-by-Step 解説

Step 1: 群の位数を素因数分解する

$(\mathbb{Z}/p\mathbb{Z})^*$ の位数 $p-1$ を $q_1^{e_1}\cdots q_k^{e_k}$ に分解。$q_i\le10^6$ なので試し割りで十分高速。

Step 2: 各素数冪の部分群に帰着

$g_i=g^{(p-1)/q_i^{e_i}}$(位数 $q_i^{e_i}$)、$y_i=y^{(p-1)/q_i^{e_i}}$ とし $g_i^x=y_i$ の $x\bmod q_i^{e_i}$ を求める。

Step 3: 桁ごとの Pohlig-Hellman

桁 $k$計算する量
$y_k$$(y\cdot g^{-x})^{q^{e-1-k}}$
$x_k$$\gamma^{x_k}=y_k$ を BSGS で解く(探索範囲 $q$)

Step 4: CRT で統合

各成分の $x\bmod q_i^{e_i}$ が求まったら中国剰余定理で $x\bmod(p-1)$ を復元する。

よくあるミス

ミス原因正しい書き方
単純な BSGS を $p\le10^{18}$ にそのまま適用$O(\sqrt p)\approx10^9$ で TLEPohlig-Hellman で滑らかさを利用
拡張ユークリッドを自作して逆元計算Python 3.8+ の pow(a,-1,m) を知らない組み込みのモジュラ逆元を使う
因数分解の上限誤解大きな $p-1$ で試し割りが終わらないと誤解最大素因数 $10^6$ 以下なので十分高速

次のステップ

  • 発展問題: $p-1$ が滑らかでない場合の Index Calculus 法との併用
  • 高速化: 因数分解を事前計算した素数篩で行う

自己評価

理解度: / /

自分の回答:

気づき・メモ: