Day 176-Q5 — 2026-10-12 | 合成数を法とする平方根(Hensel の持ち上げと中国剰余定理で、解の個数と解の 1 つを求める)

2026-10-12 赤色 Master (Phase 8+) ★★★★★★★★★ 数論(焦点帯 M3)。素数 $p$ を法とする平方根(Tonelli–Shanks)は解いたことがあっても、法が $p^k$ や合成数になると、$p\mid a$ のとき・$p=2$ のときに解の個数の数え方が変わる。素数べきごとに「$a$ の $p$ 進付値」「単数部分の平方根を Hensel で持ち上げる」に分け、CRT でまとめる。素数ごとの平方根は、$p-1$ の 2 べきが大きいと Tonelli–Shanks が遅くなるので Cipolla と使い分ける

問題

正の整数 $m$ と、$Q$ 個の整数 $a_1,\dots,a_Q$($0\le a_i<m$)が与えられる。各 $i$ について、合同式

$$x^2\equiv a_i\pmod m,\qquad 0\le x<m$$

の解の個数と、解を 1 つ出力せよ(解が無ければ解として -1)。

入力形式

m Q
a_1 a_2 ... a_Q

出力形式: $Q$ 行。$i$ 行目に「解の個数」「解の 1 つ(無ければ -1)」を空白区切りで。解が複数あるときはどれを出してもよい

制約

#制約
1$1\le m\le10^{12}$
2$1\le Q\le5\times10^4$
3$0\le a_i<m$

入出力例

入力例1

12 6
0 1 4 9 3 8

出力例1

2 0
4 1
4 4
2 9
0 -1
0 -1

($x^2\equiv0$ は $x=0,6$。$x^2\equiv1$ は $1,5,7,11$。$x^2\equiv3$ は解なし)

入力例2

16 4
1 9 0 4

出力例2

4 1
4 5
4 0
4 2

($2^4$ では単数 $a\equiv1\pmod8$ の解は 4 個。$a=0$ は $x=0,4,8,12$、$a=4$ は $x=2,6,10,14$)

入力例3

1000000000000 3
0 1 999999999999

出力例3

1000000 0
8 1
0 -1

($m=2^{12}5^{12}$。$x^2\equiv0$ は $x$ が $2^65^6=10^6$ の倍数で $10^6$ 個。$x^2\equiv1$ は $2^{12}$ で 4 個 × $5^{12}$ で 2 個 = 8 個。3 例とも総当たりと一致)

背景と出典

  • Hensel の補題: $f(y)\equiv0\pmod{p^j}$ で $f'(y)\not\equiv0\pmod p$ なら、$y\leftarrow y-f(y)/f'(y)$ で $p^{2j}$ を法とする解に持ち上がる($p$ 進数のニュートン法)。$f(y)=y^2-a$ では $f'(y)=2y$ なので、$p$ が奇数で $p\nmid a$ のときに使える
  • $p=2$ の場合: 奇数 $a$ について $y^2\equiv a\pmod{2^j}$ の解の個数は $j=1$ で 1、$j=2$ で $a\equiv1\pmod4$ なら 2、$j\ge3$ で $a\equiv1\pmod8$ なら 4(それ以外は 0)。整数論の標準的な結果で、本問では $2^{13}$ までの総当たりで確かめた
  • Tonelli–Shanks と Cipolla: どちらも素数を法とする平方根の標準的な方法。Tonelli–Shanks は $p-1=q\cdot2^s$ の $s$ について最悪 $O(s^2)$ 回の 2 乗が要り、Cipolla は $\mathbb F_p[\sqrt w]$ での $O(\log p)$ 回のべき乗で $s$ に依らない、と私は理解している。本問ではこの差を実測した($s=30$ の素数で Tonelli 5.12 s、Cipolla 3.41 s)
  • 本問は自作。素数を法とする平方根(Tonelli、Day 一覧に既出)と CRT を、素数べき・$p\mid a$・$p=2$ の場合分けまで含めた形にした

概念図

x² ≡ a (mod m): 素数べき p^k ごとに場合分けし、CRT で合わせる m = Π p^k(試し割り、1 回だけ) a ≡ 0 (mod p^k) ? p^⌊k/2⌋ 個、解 x = 0 a = p^v·a'、v は奇数 ? 解なし(0 個) y² ≡ a' (mod p^(k−v))、x = p^(v/2)·y p = 2: a' ≡ 1 (mod 8) なら y = 1 から1 ビットずつ持ち上げ(4 個。j ≤ 2 は別) 奇素数: Tonelli(s ≤ 24)/ Cipolla(s > 24)→ Hensel で p, p², p⁴, … と倍に(2 個) 個数 × p^(v/2)、全素数べきの個数の積・解は CRT はいはいいいえいいえ

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

ヒント1(方向性)

CRT により、$m=\prod p^k$ の各 $p^k$ で独立に解ける。解の個数は各素数べきでの個数の積、解は CRT で 1 つに合わせる。

ヒント2(アプローチ)

$p^k$ を法として、$a$ を $p^v\cdot a'$($p\nmid a'$)と書く。

  • $a\equiv0\pmod{p^k}$: $x$ が $p^{\lceil k/2\rceil}$ の倍数なら $x^2\equiv0$。個数は $p^{\lfloor k/2\rfloor}$
  • $v$ が奇数: 解なし
  • $v$ が偶数: $x=p^{v/2}y$ と置くと $y^2\equiv a'\pmod{p^{k-v}}$。$y$ は $p^{k-v/2}$ を法として決まるので、個数は「$p^{k-v}$ での単数の平方根の個数」$\times p^{v/2}$
ヒント3(誘導)

単数 $a'$ の平方根(法 $p^j$):

  • 奇素数: オイラーの基準で平方剰余か調べ、Tonelli–Shanks(または Cipolla)で法 $p$ の解を出し、$y\leftarrow y-(y^2-a')(2y)^{-1}$ で $p^2,p^4,\dots$ と持ち上げる。個数は 2
  • $p=2$: $j\ge3$ なら $a'\equiv1\pmod8$ のとき $y=1$ から始め、$y^2\not\equiv a'\pmod{2^{i+1}}$ なら $y\leftarrow y+2^{i-1}$ で 1 ビットずつ持ち上げる。個数は 4

模範解答 (Python)

模範解答を表示する(まず自力で解いてから)
import sys


def factorize(m):
    fs = []
    d = 2
    while d * d <= m:
        if m % d == 0:
            k = 0
            while m % d == 0:
                m //= d
                k += 1
            fs.append((d, k))
        d += 1 if d == 2 else 2
    if m > 1:
        fs.append((m, 1))
    return fs


_TS = {}


def sqrt_mod_p(a, p):
    """奇素数 p、a は p と互いに素な平方剰余。p-1 = q·2^s の s が小さければ Tonelli–Shanks、
    大きければ Cipolla(Tonelli は最悪 s^2/2 回の 2 乗が要り、s=30 で遅い)"""
    if p % 4 == 3:
        return pow(a, (p + 1) // 4, p)
    if p not in _TS:
        q, s = p - 1, 0
        while q % 2 == 0:
            q //= 2
            s += 1
        z = 2
        while pow(z, (p - 1) // 2, p) != p - 1:
            z += 1
        _TS[p] = (q, s, pow(z, q, p))
    q, s, c = _TS[p]
    if s > 24:
        return cipolla(a, p)
    m, t, r = s, pow(a, q, p), pow(a, (q + 1) // 2, p)
    while t != 1:
        i, t2 = 0, t
        while t2 != 1:
            t2 = t2 * t2 % p
            i += 1
        b = pow(c, 1 << (m - i - 1), p)
        m, c, t, r = i, b * b % p, t * b * b % p, r * b % p
    return r


def cipolla(a, p):
    """w = t^2 - a が非剰余になる t を探し、F_p[√w] で (t + √w)^((p+1)/2) を計算する"""
    t = 1
    while True:
        w = (t * t - a) % p
        if pow(w, (p - 1) // 2, p) == p - 1:
            break
        t += 1
    e = (p + 1) // 2
    x0, x1 = 1, 0
    b0, b1 = t, 1
    while e:
        if e & 1:
            x0, x1 = (x0 * b0 + x1 * b1 % p * w) % p, (x0 * b1 + x1 * b0) % p
        b0, b1 = (b0 * b0 + b1 * b1 % p * w) % p, 2 * b0 * b1 % p
        e >>= 1
    return x0


def unit_root(a, p, j):
    """p と互いに素な a について y^2 ≡ a (mod p^j) の (解の個数, 解の 1 つ)。j >= 1"""
    if p == 2:
        if j == 1:
            return 1, 1
        if j == 2:
            return (2, 1) if a % 4 == 1 else (0, None)
        if a % 8 != 1:
            return 0, None
        y = 1                                   # mod 8 の解。1 ビットずつ持ち上げる
        for i in range(3, j):                   # y は mod 2^i の解 → mod 2^(i+1) の解に
            if (y * y - a) % (1 << (i + 1)):
                y += 1 << (i - 1)
        return 4, y % (1 << j)
    if pow(a % p, (p - 1) // 2, p) != 1:
        return 0, None
    y = sqrt_mod_p(a % p, p)
    pj = p ** j
    mod = p
    while mod < pj:                             # Hensel: 精度を 2 倍ずつ(ニュートン法)
        mod = min(mod * mod, pj)
        y = (y - (y * y - a) * pow(2 * y, -1, mod)) % mod
    return 2, y


def solve_prime_power(a, p, k):
    pk = p ** k
    a %= pk
    if a == 0:
        return p ** (k // 2), 0
    v = 0
    while a % p == 0:
        a //= p
        v += 1
    if v % 2:
        return 0, None
    cnt, y = unit_root(a, p, k - v)
    if cnt == 0:
        return 0, None
    h = v // 2
    return cnt * p ** h, (p ** h * y) % pk


def main():
    data = sys.stdin.buffer.read().split()
    m, q = int(data[0]), int(data[1])
    fs = factorize(m)
    mods = [p ** k for p, k in fs]
    # CRT の係数(M/pk の pk を法とする逆元)を前計算
    coef = [(m // pk) * pow(m // pk, -1, pk) % m for pk in mods]
    out = []
    for i in range(q):
        a = int(data[2 + i])
        total, x = 1, 0
        for (p, k), c in zip(fs, coef):
            cnt, r = solve_prime_power(a, p, k)
            if cnt == 0:
                total = 0
                break
            total *= cnt
            x += r * c
        if total == 0:
            out.append("0 -1")
        else:
            out.append(f"{total} {x % m}")
    print("\n".join(out))


main()

計算量: 素因数分解 $O(\sqrt m)$ を 1 回。各クエリは素因数ごとに $O(\log p+s^2)$(Tonelli)または $O(\log p)$(Cipolla)と Hensel の $O(\log k)$ 回の逆元。$Q=10^5$(制約の 2 倍)で、$m$ が素数のとき 2.29〜3.53 s、合成数のとき 0.44〜0.73 s(Python 3.14.7、M1 MacBook Pro 8GB、負荷平均 11〜18 での実測、起動込み)。制約は余裕を見て $Q\le5\times10^4$ とした。

Step-by-Step 解説

1CRT で素数べきに分ける

$m=\prod p_j^{k_j}$ なら、$x^2\equiv a\pmod m$ は各 $p_j^{k_j}$ での合同式の組と 1 対 1。個数は積、解は $x=\sum r_j\cdot c_j$($c_j\equiv1\pmod{p_j^{k_j}}$、他の法で 0)。$c_j$ は $a$ に依らないので前計算する。

2$a\equiv0$ のとき

$x^2\equiv0\pmod{p^k}$ ⇔ $p^{\lceil k/2\rceil}\mid x$。$0\le x<p^k$ でその倍数は $p^{k-\lceil k/2\rceil}=p^{\lfloor k/2\rfloor}$ 個。入力例 3 の $10^6$ がこれ。1 個と数えるのがよくあるミス($m\le600$ の全 180300 組のうち 3925 組で誤る、実測)。

3$p\mid a$ だが $a\not\equiv0$

$a=p^va'$。$x^2$ の $p$ 進付値は偶数なので、$v$ が奇数なら解なし。$v$ が偶数なら $x=p^{v/2}y$ で $p\nmid y$ が必要、$y^2\equiv a'\pmod{p^{k-v}}$。$x$ は $p^k$ を法とするので $y$ は $p^{k-v/2}$ を法として数え、$p^{k-v}$ での 1 つの解に対し $p^{v/2}$ 通りの持ち上げ方がある。

4単数の平方根と Hensel

奇素数なら解は $\pm y$ の 2 個($p^j$ を法としても 2 個のまま)。$f(y)=y^2-a$、$f'(y)=2y$ は $p$ と互いに素なので、ニュートン法 $y\leftarrow y-f(y)/f'(y)$ が $p^j\to p^{2j}$ と精度を倍にする。$p=2$ では $f'(y)=2y$ が 2 で割れて Hensel がそのまま使えないので、mod 8 から 1 ビットずつ持ち上げ、個数は 4($\pm y,\pm y+2^{j-1}$)。

5素数を法とする平方根の使い分け

$p\equiv3\pmod4$ なら $a^{(p+1)/4}$ で一発。それ以外は Tonelli–Shanks だが、$p-1$ の 2 べき $s$ が大きいと内側のループが $O(s^2)$。$s>24$ では Cipolla に切り替える(閾値は $s=20$ で Tonelli 3.12 s・Cipolla 3.92 s、$s=30$ で 5.12 s・3.41 s という実測から決めた)。

迷った他案と採らなかった理由

案1 クエリの計算量採否理由
総当たり $x=0..m-1$$O(m)$検算用のみ$m\le600$ と $2^{13},3^8,5^5,7^4,11^3$
法 $p$ だけ見て $1+(a/p)$ 個とする$O(\log p)$不採用(誤り)180300 組中 25140 組で誤答(実測)
$p=2$ を奇素数と同じく 2 個とする—不採用(誤り)8396 組で誤答(実測)。入力例 2 で 4 個のところを 2 個
$a\equiv0$ を 1 個とする—不採用(誤り)3925 組で誤答(実測)
Tonelli–Shanks だけ$O(\log p+s^2)$不採用($s$ が大きいと遅い)$p=901943132161$($s=30$)で 5.12 s($Q=10^5$、実測)
Cipolla だけ$O(\log p)$($\mathbb F_{p^2}$ の乗算)不採用($s$ が小さいと遅い)$p=999999999989$($s=2$)で 5.61 s、Tonelli は 2.24 s(実測)
$s$ で使い分け(本解)小さい方採用最悪 3.53 s($Q=10^5$)

前提が変わったら

  • $m\le10^{18}$ なら試し割りでは分解できない。Pollard の ρ(Day 一覧に既出)で分解し、残りは同じ
  • 解をすべて列挙するなら、各素数べきの解の集合($a\equiv0$ なら $p^{\lfloor k/2\rfloor}$ 個)の直積を CRT で組む。個数は最大で $m$ 程度になりうるので、出力量が制約になる
  • $x^k\equiv a$(一般の $k$ 乗根)なら、法 $p$ での $k$ 乗根(Adleman–Manders–Miller)と、$p\mid k$ のときの持ち上げの場合分けが増える

よくあるミス

ミス原因正しい書き方
$a\equiv0\pmod{p^k}$ の解を 1 個とする「$x=0$ だけ」と思い込む$p^{\lfloor k/2\rfloor}$ 個。$m=16$ なら 0,4,8,12 の 4 個
$p=2$ にも Hensel をそのまま使う$f'(y)=2y$ が 2 の倍数mod 8 から 1 ビットずつ。個数は 4
$p\mid a$ で $v$ の偶奇を見ない単数の場合の式を流用$v$ 奇数なら解なし。偶数なら $x=p^{v/2}y$
$y$ の持ち上げの個数 $p^{v/2}$ を掛け忘れる$y$ と $x$ の法の違いを見落とす$y$ は $p^{k-v/2}$ を法として数える
Tonelli の非剰余 $z$ をクエリごとに探す関数をそのまま呼ぶ素数ごとに 1 回だけ。$s=30$ で 10.77 s → 5.30 s(p ** j の再計算をやめた分も含む、実測)

次のステップ

  • 発展1: 解をすべて列挙する版を作り、個数の式と照合する
  • 発展2: $x^3\equiv a\pmod{p^k}$ で、$p=3$ のときの持ち上げの場合分けを総当たりで調べる
  • 発展3: Pollard の ρ と組み合わせて $m\le10^{18}$ に広げる

検証ログ(本セッションで実行した数値)

  • 総当たりとの照合(実測): $m=1\sim600$ のすべての $a$(計 180300 組)で、解の個数が一致し、出力した解が $x^2\equiv a$ を満たす。素数べき $2^{13},3^8,5^5,7^4,11^3$ の全 $a$ でも一致
  • Cipolla の経路(実測): 使い分けの閾値を外して常に Cipolla にした版でも $m\le300$ の全 45150 組が一致(閾値 $s>24$ は小さい素数では通らないため)
  • 大きな $m$(実測、$Q=10^5$、半分は $r^2\bmod m$): $m=901943132161,999999999989,2^{39},200560490130(=2\cdot3\cdots31),999999999999,10^{12}$ で、出力した解がすべて合同式を満たし、平方数のクエリで「解なし」を返したものは 0
  • 誤った方法(実測、$m\le600$ の 180300 組): 法 $p$ だけ見る 25140、$p=2$ を 2 個とする 8396、$a\equiv0$ を 1 個とする 3925 組で誤答
  • 実行時間(実測、$Q=10^5$、起動込み): 素数 $m$ で Tonelli のみ 2.24〜5.12 s、Cipolla のみ 3.41〜5.61 s、使い分け 2.29〜3.53 s。合成数 $m$ で 0.44〜0.73 s
  • 扱っていない範囲: $m>10^{12}$(Pollard の ρ)、全解の列挙、一般の $k$ 乗根

自己評価

自分の回答

気づき・メモ