Day 173-Q5 — 2026-10-09 | 安全素数でない p の離散対数(大きい素因数 q は指数計算法、残りは Pohlig–Hellman、CRT で合わせる)

2026-10-09 赤色 Master (Phase 8+) ★★★★★★★★★ 数論(焦点帯 M3)。Day170-Q5 は安全素数 $p=2q+1$ に限り、法 2 はオイラーの規準、法 $q$ は指数計算法で解いた。Day169-Q5 の Pohlig–Hellman は $p-1$ が小さい素因数だけのときに使った。今回は両方が混ざった $p-1=s\cdot q$($s$ は小さい素因数だけ、$q$ は大きい素数)を扱う

問題

素数 $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$(素数)なら体なので掃き出しがそのまま使える。本問は自作。

概念図

入力例 2: p − 1 = 1264662 = 2 · 3² · 7 · 10037 を分けて解き、CRT で戻す g^x ≡ h (mod p) の x mod (p−1) 小さい素数冪 2, 3², 7 : Pohlig–Hellman 位数 r の元 g^((p−1)/r) で r 進の 1 桁ずつ BSGS は √r ≤ 100 歩 3² を法 3 だけで解くと誤答(53/400) 大きい素数 q = 10037 : 指数計算法 g^e mod p が 1000-smooth な e を集める 168 個の log を法 q(素数)で掃き出し h·g^t が smooth な t を探して log h mod q CRT → x = 440092(h = 2)

ヒント(クリックで段階的に開示)

ヒント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 解説

1$p-1$ を小さい素因数と $q$ に分ける

$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 クエリぶんは実測)。

2小さい素数冪は Pohlig–Hellman

$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 個、実測)。

3大きい素数 $q$ は指数計算法

ランダムな $e$ で $g^e\bmod p$ を作り、因子基底だけで割り切れるものを集める。1 本の関係式は $e\equiv\sum c_i\log b_i\pmod{p-1}$。これを法 $q$ に落とすと、法が素数なので掃き出しができる。法 $p-1$ のままだと、ピボットが $p-1$ と互いに素でないとき逆元が無い。

4掃き出しで決まらない変数

関係式に一度も出てこない素数(大きめの素数)は、対数が決まらない。掃き出し後、「その変数のピボット行が他の自由変数に依存しない」ものだけを分かった対数とする。クエリで smooth な値が見つかっても、分からない素数を含むなら捨てて別の $t$ を試す。

5各 $h$ の対数 mod q

$h\,g^t\bmod p$ が smooth で $\prod b_i^{c_i}$ なら、$\log h+t\equiv\sum c_i\log b_i\pmod q$。

6CRT

$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、例 2 440092 969474、例 3 793341950834。いずれも $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$、原典の確認

自己評価

自分の回答

気づき・メモ