問題
$T$ 個の整数 $n_1,\dots,n_T$ が与えられる。各 $n$ について、次を出力せよ。
- $a^2+b^2=n$ を満たす整数の組 $(a,b)$(符号・順序を区別する)の個数 $r_2(n)$
- $r_2(n)>0$ なら、$0\le a\le b$ かつ $a^2+b^2=n$ を満たす組を 1 つ
$r_2(n)=0$ なら 0 だけ、そうでなければ r_2(n) a b を 1 行に出力する。組が複数あるときはどれを出力してもよい。
入力形式
T
n_1
...
n_T
制約
| # | 条件 |
|---|---|
| 1 | $1\le T\le100$ |
| 2 | $1\le n_i\le10^{18}$ |
入出力例
入力例1
5
25
3
9
50
1出力例1
12 3 4
0
4 0 3
12 1 7
4 0 1($25=0^2+5^2=3^2+4^2$。符号と順序を区別すると $(\pm5,0),(0,\pm5),(\pm3,\pm4),(\pm4,\pm3)$ の 12 組。$50$ は 1 7 でも 5 5 でもよい。$9=3^2$ は $3$ が 4k+3 型で偶数乗なので表せ、$3$ は奇数乗なので 0)
入力例2
2
999999999999999999
1000000000000000000出力例2
0
76 472103424 881543168($999999999999999999=3^4\cdot7\cdot11\cdot13\cdot19\cdot37\cdot52579\cdot333667$ は 4k+3 型の $7,11,19$ が奇数乗。$10^{18}=2^{18}5^{18}$ で $r_2=4\cdot19=76$。2 例とも、出力した組の $a^2+b^2$ を検算)
背景と出典
次はいずれも私の記憶による(本セッションでは原典を参照していない。式は $n\le2\times10^5$ の全数で数え上げと照合した)。
- Fermat の二平方定理: 奇素数 $p$ が $a^2+b^2$ で表せるのは $p\equiv1\pmod4$ のときに限る
- Jacobi の二平方の公式: $r_2(n)=4\,(d_1(n)-d_3(n))$($d_j$ は $j\bmod4$ の約数の個数)。分解 $n=2^{e_0}\prod p^{e}\prod q^{f}$($p\equiv1$、$q\equiv3$)で書くと、どれかの $f$ が奇数なら $0$、そうでなければ $4\prod(e+1)$
- Cornacchia のアルゴリズム(1908): $x^2\equiv-1\pmod p$ の解 $x$ から始めて $(p,x)$ にユークリッドの互除法を行い、最初に $\sqrt p$ 未満になった余り $a$ で $p=a^2+b^2$ が得られる
組の構成は、ガウス整数 $\mathbb Z[i]$ で $p=(a+bi)(a-bi)$ と分かれることを使う。$n$ の各素因数の「$a+bi$」を掛け合わせれば $n=|z|^2$ となる $z$ が得られる。本問は自作。
概念図
ヒント(クリックで段階的に開示)
ヒント1(方向性)
$a^2+b^2=|a+bi|^2$。$|zw|^2=|z|^2|w|^2$ なので、二平方和で表せる数どうしの積も二平方和で表せる($(a^2+b^2)(c^2+d^2)=(ac-bd)^2+(ad+bc)^2$)。素数ごとに表して掛ければよい。
ヒント2(アプローチ)
素因数ごとに場合分け: $2=1^2+1^2$、$p\equiv1\pmod4$ は Cornacchia、$q\equiv3\pmod4$ は偶数乗なら $q^{f/2}$ をそのまま掛ける($q^f=(q^{f/2})^2+0^2$)、奇数乗なら表せない。$10^{18}$ の素因数分解は Miller–Rabin と Pollard の rho。
ヒント3(誘導)
$x^2\equiv-1\pmod p$ は、平方非剰余 $c$($c^{(p-1)/2}\equiv-1$)を探して $x=c^{(p-1)/4}$。
a, b = p, x
while b > isqrt(p):
a, b = b, a % b
return b, isqrt(p - b * b) # p = b^2 + (残り)^2
個数は $4\prod_{p\equiv1}(e+1)$。
模範解答 (Python)
模範解答を表示する(まず自力で解いてから)
import sys
import random
from math import gcd, isqrt
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): # この 12 個の底で n < 3×10^23 まで決定的(記憶)
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):
# Pollard の rho(Brent 版、gcd を 128 回分まとめて取る)
if n % 2 == 0:
return 2
while True:
y, c, m = random.randrange(1, n), random.randrange(1, n), 128
g = r = q = 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:
g = 1
while g == 1:
ys = (ys * ys + c) % n
g = gcd(abs(x - ys), n)
if g != n:
return g
def factor(n, res):
if n == 1:
return
if is_prime(n):
res[n] = res.get(n, 0) + 1
return
r = isqrt(n)
if r * r == n: # 平方数は先に開く(p^2 の rho は実測で遅かった)
factor(r, res)
factor(r, res)
return
d = rho(n)
factor(d, res)
factor(n // d, res)
def two_squares_prime(p):
# p ≡ 1 (mod 4) を a^2 + b^2 に。x^2 ≡ -1 (mod p) を作り、Cornacchia(ユークリッドの互除法を √p 未満で止める)
c = 2
while pow(c, (p - 1) // 2, p) != p - 1: # 平方非剰余 c を探す(半分の c が当たる)
c += 1
x = pow(c, (p - 1) // 4, p) # x^2 = c^((p-1)/2) = -1
a, b = p, x
r = isqrt(p)
while b > r:
a, b = b, a % b
return b, isqrt(p - b * b)
def gmul(x, y):
# ガウス整数の積 (a+bi)(c+di)
return (x[0] * y[0] - x[1] * y[1], x[0] * y[1] + x[1] * y[0])
def gpow(x, e):
r = (1, 0)
while e:
if e & 1:
r = gmul(r, x)
x = gmul(x, x)
e >>= 1
return r
def solve(n):
f = {}
small = (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47)
for p in small: # 小さい素因数は割り算で先に取る(rho の負担を減らす)
while n % p == 0:
f[p] = f.get(p, 0) + 1
n //= p
factor(n, f)
count = 4
z = (1, 0)
for p, e in f.items():
if p % 4 == 3:
if e % 2:
return 0, None # 4k+3 型の素数が奇数乗 → 表せない
z = (z[0] * p ** (e // 2), z[1] * p ** (e // 2))
elif p == 2:
z = gmul(z, gpow((1, 1), e)) # 2 = (1+i)(1-i)
else:
count *= e + 1 # r2(n) = 4 × Π(e+1)(p ≡ 1 mod 4)
z = gmul(z, gpow(two_squares_prime(p), e))
a, b = sorted((abs(z[0]), abs(z[1])))
return count, (a, b)
def main():
data = sys.stdin.buffer.read().split()
t = int(data[0])
out = []
for i in range(1, t + 1):
n = int(data[i])
cnt, ab = solve(n)
out.append(f"{cnt} {ab[0]} {ab[1]}" if cnt else "0")
print("\n".join(out))
if __name__ == "__main__":
main()
計算量: 分解は Pollard の rho で最小の素因数 $p$ に対し期待 $O(p^{1/2})$ 回の乗算($n\le10^{18}$ なら最悪でも $p\approx10^9$ で数万回)。Cornacchia は互除法なので $O(\log p)$、平方非剰余探しは期待 2 回の累乗。$T=100$、すべて $10^9$ 前後の 2 素数の積という最悪寄りの入力で 0.85〜0.96 s(Python 3.14.7、M1 MacBook Pro、実測)。
Step-by-Step 解説
$(a^2+b^2)(c^2+d^2)=(ac-bd)^2+(ad+bc)^2$。これはガウス整数 $(a+bi)(c+di)$ の絶対値の 2 乗を取っただけ。だから $n$ を素因数ごとに表せれば、掛けて組み立てられる。
- $2=|1+i|^2$
- $p\equiv1\pmod4$: $p=|a+bi|^2$ と表せる(Fermat)。Cornacchia で $a,b$ を求める
- $q\equiv3\pmod4$: $\mathbb Z[i]$ でも素数のまま。偶数乗なら $q^f=|q^{f/2}|^2$、奇数乗なら $n$ は表せない
平方非剰余 $c=2$($2^6=64\equiv-1\pmod{13}$)。$x=2^3=8$($8^2=64\equiv-1$)。$\sqrt{13}\approx3.6$。互除法 $(13,8)\to(8,5)\to(5,3)$ で $3<3.6$ になったので止め、$a=3$、$b=\sqrt{13-9}=2$。$13=3^2+2^2$。
$p\equiv1$ の素因数 $p^e$ は、$\mathbb Z[i]$ で $\pi^j\bar\pi^{e-j}$($j=0..e$)の $e+1$ 通りに分けられる。単数 $\pm1,\pm i$ の 4 倍で $4\prod(e+1)$。$2$ と偶数乗の $q$ は分け方が 1 通りなので掛からない。
小さい素数(47 以下)は割り算で先に取る。残りが素数なら終わり、平方数なら平方根を分解($p^2$ を rho にかけると、100 個で 1.37 s かかったものが 0.04 s になった。実測)、それ以外は rho。
迷った他案と採らなかった理由
| 案 | 計算量 | 採否 | 理由 |
|---|---|---|---|
| $a=0..\sqrt n$ のループ | $O(\sqrt n)$ | 検算用のみ | $\sqrt{10^{14}}=10^7$ 回で 2.07 s(実測)→ $10^{18}$ は 1 問 $10^9$ 回、約 200 s(導出) |
| 試し割りで分解 | $O(\sqrt n)$ | 不採用 | $10^9$ 前後の 2 素数の積で $10^9/\ln$ 回程度、同じく間に合わない(導出) |
| $x^2\equiv-1$ を Tonelli–Shanks で解く | $O(\log^2p)$ | 不採用 | $p\equiv1\pmod4$ なら $c^{(p-1)/4}$ の 1 回の累乗で足りる |
| Hermite–Serret(連分数で $p=a^2+b^2$) | $O(\log p)$ | 不採用 | Cornacchia と中身は同じ互除法。止める条件が「$\sqrt p$ 未満」と単純な Cornacchia を採用 |
| rho+Cornacchia+ガウス整数の積(本解) | 期待 $O(n^{1/4})$ | 採用 | 最悪寄りの入力 100 問で 0.96 s |
前提が変わったら:
- すべての組を列挙するなら、各 $p^e$ について $\pi^j\bar\pi^{e-j}$ を全通り掛けて単数倍する。個数は $r_2(n)$ 個で、$n\le10^{18}$ では 4k+1 型の小さい素数 11 個の積($5\cdot13\cdots97\approx9.9\times10^{16}$)だけでも $4\cdot2^{11}=8192$、指数を振れば数万に届く(概算)
- $a^2+db^2=n$($d\ne1$)なら Cornacchia は一般の形で使えるが、合成数 $n$ への組み立ては $\mathbb Z[\sqrt{-d}]$ が一意分解でないと単純な積では済まない
- $T=10^5$ なら rho の定数が重く Python では厳しい。$n\le10^{7}$ 程度なら最小素因数の篩で分解する
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| $r_2(n)=4\,d(n)$(全約数の個数)とする | Jacobi の公式の $d_1-d_3$ を忘れる | $n\le2\times10^5$ の全数中 181308 個で誤答(実測) |
| 4k+3 型の奇数乗を見落とす | $p\equiv1$ の因子だけ数える | 表せない $n$($2\times10^5$ 以下で 153550 個)すべてで誤答(実測) |
| 互除法を余り 0 まで回す | 「最大公約数」の癖 | $\sqrt p$ 未満になった最初の余りで止める。0 まで回すと $\gcd(p,x)=1$ しか残らない |
| $(a+bi)^e$ の代わりに $a^e,b^e$ を掛ける | 積の公式の取り違え | ガウス整数として累乗する($5^2=25$ は $(2+i)^2=3+4i$ で $3^2+4^2$) |
| $p^2$ をそのまま rho にかける | 平方数の特別扱いを忘れる | 正しいが遅い。100 個で 1.37 s → 平方根を先に取って 0.04 s(実測) |
次のステップ
発展1: すべての組 $(a,b)$ を列挙する($r_2(n)$ 個)
- 発展2: 三平方和(Legendre の三平方定理: $4^a(8b+7)$ 型以外は表せる)と、その構成
- 発展3: 原典(Cornacchia 1908、Jacobi の二平方の公式)を確認する(本セッションでは未確認)
検証ログ(本セッションで実行した数値)
全数照合(実測): $1\le n\le2\times10^5$ のすべてで、$r_2(n)$ が全組の数え上げと一致し、出力した組が $a^2+b^2=n$・$0\le a\le b$ を満たす(表せない $n$ は 153550 個)
- ランダム照合(実測): $n\le10^{10}$ の 200 個(半数は $a^2+b^2$ の形で作成、表せるもの 113)で、$a$ のループによる個数と一致
- 大きい $n$(実測): 素因数を指定して作った $n\le10^{18}$ の 300 個で、個数が既知の分解からの値と一致し、組の検算もすべて合格(1 問の最大 0.019 s)。下の計測用 500 個の出力の組もすべて検算合格
- 誤った方法(実測): $4d(n)$ は 181308 個、4k+3 の奇数乗の見落としは 153550 個で誤答
- 入力例(実測): 例 1・例 2 とも上記のとおり(組は検算)
- 実行時間(実測、$T=100$、負荷平均 3〜8): $10^9$ 前後の 4k+1 型 2 素数の積 0.91〜0.96 s、4k+1 型と 4k+3 型の積 0.81〜0.85 s、4k+3 型素数の 2 乗 0.04 s(平方根を先に取らない版は 1.37 s)、$10^{18}$ 前後の 4k+1 型素数 0.05 s、一様乱数 0.06〜0.07 s
- 扱っていない範囲: 全組の列挙、$a^2+db^2$、$T=10^5$ 規模、原典の確認