問題
$Q$ 個の問い合わせがある。各問い合わせでは整数 $a,m$ が与えられる。$x^2\equiv a\pmod m$、$0\le x<m$ を満たす $x$ の個数と、そのうち最小のものを出力せよ(解が無ければ 0 -1)。
入力形式
Q
a_1 m_1
...
a_Q m_Q
制約
| # | 制約 |
|---|---|
| 1 | $1\le Q\le200$ |
| 2 | $1\le m_i\le10^{18}$、$0\le a_i<m_i$、$\gcd(a_i,m_i)=1$($m_i=1$ のときは $a_i=0$) |
| 3 | 出力は 1 問い合わせ 1 行に 個数 最小の解 |
入出力例
入力例1
4
1 8
2 7
4 15
3 5出力例1
4 1
2 3
4 2
0 -1($x^2\equiv1\pmod8$ は $1,3,5,7$ の 4 個。奇素数なら解は 2 個だが、$2^3$ 以上では 4 個になる。$x^2\equiv4\pmod{15}$ は $2,7,8,13$)
入力例2
3
2809 614889782588491410
1 1000000000000000000
2 999999999999999989出力例2
16384 53
8 1
0 -1(1 行目の法は 47 までの素数 15 個の積で、$2$ の因子は解 1 個・奇素数 14 個はそれぞれ 2 個、合わせて $2^{14}=16384$ 個。2 行目は $2^{18}\cdot5^{18}$ で $4\times2=8$ 個。3 行目は素数 $p\equiv5\pmod8$ で 2 は平方非剰余。3 例とも全探索・独立な検算と一致)
背景と出典
- 法 $p$ の平方根の Tonelli–Shanks 法、Hensel の補題による持ち上げ、2 べきでの解の個数($2^e$, $e\ge3$ で 4 個)は数論の標準的な事実(教科書名は挙げない。本セッションでは原典を確認していない)
- Pollard の ρ 法(1975)と Brent の改良(1980)は記憶による年号で、未確認。Miller–Rabin を基数 2〜37 の 12 個で回すと $3.3\times10^{24}$ 未満で決定的、という事実も記憶による(未確認)
- 本問の問題文・制約・入力例は自作
概念図
ヒント(クリックで段階的に開示)
ヒント1(方向性)
CRT(中国剰余定理)で、$m=\prod p_i^{e_i}$ なら「法 $m$ の解」と「各 $p_i^{e_i}$ の解の組」が 1 対 1 に対応する。個数は積、解は組み合わせで作れる。まず $m$ を素因数分解する($10^{18}$ なので試し割りでは無理)。
ヒント2(アプローチ)
- 奇素数 $p$: $a^{(p-1)/2}\equiv1$ なら法 $p$ の解 $\pm r$ があり(Tonelli–Shanks)、$\gcd(a,p)=1$ なので $p^e$ へ 1 通りずつ持ち上がる($2r\not\equiv0$ だから Hensel が使える)。解は 2 個
- $p=2$: $e=1$ は 1 個、$e=2$ は $a\equiv1\pmod4$ のとき 2 個、$e\ge3$ は $a\equiv1\pmod8$ のとき 4 個($\pm r,\ \pm r+2^{e-1}$)
ヒント3(誘導)
- Hensel: $r\leftarrow r-(r^2-a)\cdot(2r)^{-1}\bmod p^{k}$ で、正しい桁数が倍々に伸びる(ニュートン法)
- 2 べき: $r=1$ から始め、$k=3,4,\dots,e-1$ で $r^2\not\equiv a\pmod{2^{k+1}}$ なら $r$ に $2^{k-1}$ を足す
- 素因数分解は Miller–Rabin で素数判定し、合成数なら Brent 版 Pollard ρ(gcd を 128 歩ごとにまとめる)で割る
模範解答 (Python)
模範解答を表示する(まず自力で解いてから)
import sys
from math import gcd
from random import randrange
def is_prime(n):
if n < 2:
return False
for p in (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37):
if n % p == 0:
return n == p
d, s = n - 1, 0
while d % 2 == 0:
d //= 2; s += 1
for a in (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37): # n < 3.3e24 で決定的
x = pow(a, d, n)
if x == 1 or x == n - 1:
continue
for _ in range(s - 1):
x = x * x % n
if x == n - 1:
break
else:
return False
return True
def rho(n):
"""Brent 版 Pollard ρ。gcd を 128 歩ごとにまとめて取る"""
if n % 2 == 0:
return 2
while True:
c = randrange(1, n)
y, m, g, r, q = randrange(n), 128, 1, 1, 1
while g == 1:
x = y
for _ in range(r):
y = (y * y + c) % n
k = 0
while k < r and g == 1:
ys = y
for _ in range(min(m, r - k)):
y = (y * y + c) % n
q = q * abs(x - y) % n
g = gcd(q, n)
k += m
r *= 2
if g == n: # まとめすぎて n になったら 1 歩ずつ戻す
g = 1
while g == 1:
ys = (ys * ys + c) % n
g = gcd(abs(x - ys), n)
if g != n:
return g
def factor(n, out):
for p in (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47):
while n % p == 0:
out[p] = out.get(p, 0) + 1
n //= p
stack = [n] if n > 1 else []
while stack:
x = stack.pop()
if x == 1:
continue
if is_prime(x):
out[x] = out.get(x, 0) + 1
continue
d = rho(x)
stack.append(d); stack.append(x // d)
return out
def sqrt_mod_prime(a, p):
"""Tonelli–Shanks。a は p の平方剰余(p 奇素数)"""
if p % 4 == 3:
return pow(a, (p + 1) // 4, p)
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
m, c, t, r = s, pow(z, q, p), pow(a, q, p), pow(a, (q + 1) // 2, p)
while t != 1:
i, tt = 0, t
while tt != 1:
tt = tt * tt % 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 roots_prime_power(a, p, e):
"""x^2 ≡ a (mod p^e) の解(gcd(a,p)=1)"""
pe = p ** e
a %= pe
if p == 2:
if e == 1:
return [1]
if e == 2:
return [1, 3] if a % 4 == 1 else []
if a % 8 != 1:
return []
r = 1 # 法 8 で 1。k ビット目ずつ直す
for k in range(3, e):
if (r * r - a) % (1 << (k + 1)):
r += 1 << (k - 1)
h = 1 << (e - 1)
return sorted({r % pe, (-r) % pe, (r + h) % pe, (-r + h) % pe})
if pow(a % p, (p - 1) // 2, p) != 1:
return []
r = sqrt_mod_prime(a % p, p)
k = 1
while k < e: # Hensel(ニュートン)で桁を倍々に
k = min(2 * k, e)
mod = p ** k
r = (r - (r * r - a) * pow(2 * r, -1, mod)) % mod
return [r, pe - r] if r != pe - r else [r]
def main():
data = sys.stdin.buffer.read().split()
q = int(data[0])
out = []
for i in range(q):
a, m = int(data[1 + 2 * i]), int(data[2 + 2 * i])
if m == 1:
out.append("1 0")
continue
fs = factor(m, {})
sols, mod = [0], 1
for p, e in sorted(fs.items()):
rs = roots_prime_power(a, p, e)
if not rs:
sols = []
break
pe = p ** e
inv = pow(mod, -1, pe) # CRT: x = s + mod * ((r - s) * inv mod pe)
sols = [s + mod * ((r - s) * inv % pe) for s in sols for r in rs]
mod *= pe
if sols:
out.append(f"{len(sols)} {min(sols)}")
else:
out.append("0 -1")
print("\n".join(out))
main()
計算量: 素因数分解は 1 個あたり期待 $O(m^{1/4})$ 回の乗算(Pollard ρ)、平方根は素数べきごとに $O(\log^2 p)$、組み合わせは解の個数(最大 $2^{15}$ 程度)に比例。$Q=100$ で、$5\times10^8$〜$10^9$ の素数 2 つの積(ρ の最悪に近い)0.44 s、$10^{17}$〜$10^{18}$ の乱数 0.03 s、解が 16384 個ずつある素数 15 個の積 0.26 s(Python 3.14.8、Apple M5 Pro、起動込み)。
Step-by-Step 解説
1CRT で分ける
$m=\prod p_i^{e_i}$ なら、$x\bmod m$ と $(x\bmod p_i^{e_i})_i$ は 1 対 1。$x^2\equiv a$ は各法で独立に成り立てばよいので、解の集合は各素数べきの解の集合の直積。個数は積で、どこか 1 つでも 0 なら全体も 0。
2奇素数べき
$\gcd(a,p)=1$ で $a$ が法 $p$ の平方剰余なら、法 $p$ の解は $\pm r$ の 2 個。$f(x)=x^2-a$ の微分 $2r$ は $p$ で割り切れないので、Hensel の補題で各解が法 $p^e$ の解にちょうど 1 つずつ持ち上がる。ニュートン法 $r\leftarrow r-f(r)/f'(r)$ は正しい桁数を倍にするので、$\lceil\log_2e\rceil$ 回で済む。
32 べき
奇数 $x$ の平方は法 8 で必ず 1。だから $e\ge3$ では $a\equiv1\pmod8$ が必要十分で、解は $\pm r,\ \pm r+2^{e-1}$ の 4 個($(r+2^{e-1})^2=r^2+r2^e+2^{2e-2}\equiv r^2$)。$r$ は法 $2^{k+1}$ で合わなければ $2^{k-1}$ を足して直す($(r+2^{k-1})^2\equiv r^2+r2^k\pmod{2^{k+1}}$ で、$r$ が奇数なので $2^k$ の桁が反転する)。
4素因数分解
$10^{18}$ の試し割りは $10^9$ 回で無理。小さい素数で割ったあと、Miller–Rabin で素数なら終わり、合成数なら Pollard ρ で約数を 1 つ見つけて両方を再帰。ρ は $x\mapsto x^2+c$ の列の衝突を $\gcd(|x-y|,n)$ で見つける方法で、最小の素因数 $p$ について期待 $O(\sqrt p)$ 歩。gcd を 128 歩ごとにまとめ、まとめすぎて $n$ になったら 1 歩ずつ戻る(Brent の工夫)。
5組み合わせて最小を取る
CRT を 1 素数べきずつ足していく: これまでの解 $s$(法 $M$)と新しい解 $r$(法 $p^e$)から $x=s+M\cdot((r-s)M^{-1}\bmod p^e)$。全組み合わせのリストを作り最小を取る。解の個数は、$m\le10^{18}$ だと $2^3$ と奇素数 13 個($3..43$)の $4\cdot2^{13}=32768$ が最大(奇素数 14 個に $2^3$ を掛けると $2.5\times10^{18}$ で超える)。
迷った他案と採らなかった理由
| 案 | 計算量 | 採否 | 理由 |
|---|---|---|---|
| $0..m-1$ を全部試す | $O(m)$ | 検算用のみ | $m\le1500$ の全 684182 問い合わせで使った |
| 法 $p$ の根 $\pm r$ をそのまま法 $p^e$ の解にする(Hensel を忘れる) | — | 不採用(誤り) | 684182 問い合わせ中 21080 で誤る(実測) |
| 2 べきも奇素数と同じく解 2 個 | — | 不採用(誤り) | 5913 で誤る(実測)。$2^e$($e\ge3$)は 4 個 |
| Cipolla 法で法 $p$ の平方根 | $O(\log p)$ | 不採用 | 正しく速いが、$p\equiv3\pmod4$ の近道と Tonelli–Shanks で十分速かった(素因数分解が律速) |
| 試し割り $\sqrt m$ | $O(10^9)$ | 不採用 | $10^{18}$ では間に合わない |
| Pollard ρ(Brent)+Miller–Rabin(本解) | 期待 $O(m^{1/4})$ | 採用 | 最悪に近い半素数 100 個で 0.44 s |
前提が変わったら
- $\gcd(a,m)\ne1$ を許すなら、Day176-Q5 の場合分け($a\equiv0$ なら $p^{\lfloor e/2\rfloor}$ 個、付値が奇数なら 0 個、偶数なら単数部分の解×$p^{v/2}$ 個)が要る。解の個数は $p^{\lfloor e/2\rfloor}$ で巨大になりうるので「全部作る」は無理になり、最小の解だけを求める工夫が要る
- $Q$ が $10^5$なら、半素数での ρ(1 個 4 ms 程度)が律速で 400 s 級になる。Python では $m\le10^{18}$ の大量の素因数分解は厳しく、C++ か、$m$ の素因数分解が入力で与えられる形にする
- 解の和や個数だけなら全組み合わせを作らずに積で出せる。最小の解は組み合わせが要る(CRT の値は成分ごとに単調でない)
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| $2^e$ の解を 2 個とする | 奇素数と同じと思う | $e\ge3$ は 4 個、$e=2$ は 2 個、$e=1$ は 1 個 |
| Hensel を 1 桁ずつ $e$ 回 | 間違いではないが遅い | ニュートン法で倍々 |
| ρ の gcd を毎歩取る | 素直に書く | まとめて取り、$n$ になったら戻る |
| Miller–Rabin の基数を少なくする | 「ほぼ確実」で済ませる | $10^{18}$ 級は 2〜37 の 12 基数で決定的(記憶による事実) |
CRT の途中で $M$ の逆元を毎回 pow で取り直さない/取り違える | $M$ と $p^e$ を逆にする | $M^{-1}\bmod p^e$ |
次のステップ
- 発展1: $\gcd(a,m)\ne1$ を許して最小の解だけを求める(解の個数が巨大でも、各素数べきの解を等差数列の和集合として持ち、CRT の最小値を探す)
- 発展2: $x^k\equiv a$($k$ 乗根)に広げる。法 $p$ では離散対数(Day174-Q5)や Adleman–Manders–Miller 法と記憶している方法が要る
- 発展3: ρ を Montgomery 乗算風に書き換え、半素数 100 個の 0.44 s がどこまで縮むか測る
検証ログ(本セッションで実行した数値)
- 全探索との一致(実測): $1\le m\le1500$、$0\le a<m$、$\gcd(a,m)=1$ の全 684182 問い合わせで、個数と最小の解が $0..m-1$ の全探索と全件一致(解の個数の分布: 0 個 478107、1 個 2、2 個 109886、4 個 79388、8 個 15273、16 個 1510、32 個 16)
- 独立な検算(実測): 各素数べきが $2\times10^4$ 以下の素数べきの積で $10^{12}<m\le10^{18}$ の 400 問い合わせ。素数べきごとの解を総当たりで作って CRT したものと、解の集合が全件一致(解の個数は最大 512)
- 誤った方法(実測、同じ 684182 問い合わせ): Hensel を忘れる 21080、2 べきを 2 個とする 5913 で誤る
- 時間(実測、起動込み、$Q=100$): 半素数($5\times10^8$〜$10^9$ の素数 2 つ)0.44 s、$10^{17}$〜$10^{18}$ の乱数 0.03 s、素数 15 個の積(解 16384 個ずつ)0.26 s、$3^{37}$・$2^{59}$・$(10^9+7)^2$ と大きな素数の混在 0.02 s。全出力で(最小の解)$^2\equiv a$ を確認
- 扱っていない範囲: 原典の確認、$\gcd(a,m)\ne1$、$k$ 乗根