全単射で #divisors(n/2) に帰着する偶数約数カウント
from __future__ import annotations
import os
def divisors(n: int) -> int:
"""
n の約数のうち 2 で割り切れるものの個数を返す。
【全単射による帰着】
A = { d : d|n, 2|d } ←→ B = { k : k | (n//2) }
写像 φ: d → d/2 は A→B の全単射。
よって |A| = |B| = #divisors(n // 2)
Time Complexity: O(√n)
Space Complexity: O(1)
"""
if n % 2 != 0: # ① 奇数なら偶数約数はゼロ
return 0
m: int = n // 2 # ② 全単射により n//2 の約数カウントに帰着
count: int = 0
i: int = 1
while i * i <= m: # ③ i=1 から √m まで走査
if m % i == 0:
count += 1 if i * i == m else 2 # 完全平方→+1, それ以外→+2
i += 1
return count
if __name__ == "__main__":
fptr = open(os.environ["OUTPUT_PATH"], "w")
t: int = int(input().strip())
for _ in range(t):
fptr.write(str(divisors(int(input().strip()))) + "\n")
fptr.close()
| アプローチ | 時間 | 空間 | 備考 |
|---|---|---|---|
| ✅ 全単射 + √走査 | O(√n) | O(1) | 本実装。数学的に最適。 |
| 全約数列挙 + フィルタ | O(√n) | O(1) | 同等だが定数倍わずかに大 |
| 線形スキャン | O(n) | O(1) | 大 n では TLE リスク |
| n | 偶数約数 | m=n/2 の約数 | 期待値 | ポイント |
|---|---|---|---|---|
| 1 | — | — | 0 ✅ | 奇数の基底ケース |
| 2 | 2 | 1 | 1 ✅ | 最小偶数 |
| 9 | — | — | 0 ✅ | 奇数(サンプル) |
| 8 | 2, 4, 8 | 1, 2, 4 | 3 ✅ | サンプル入力 |
| 16 | 2,4,8,16 | 1,2,4,8 | 4 ✅ | 2 の冪 |
| 36 | 2,4,6,12,18,36 | 1,2,3,6,9,18 | 6 ✅ | 完全平方 n=36(√n=6) |
Q1. なぜ偶数約数の個数 = n/2 の約数の個数?
写像 φ: d→d/2 が全単射になるためです。d|n かつ 2|d ⟺ d/2|n/2 が成立します。
Q2. 完全平方のとき count+=1 とする理由は?
i²=m のとき i=m/i なので同一の約数を 2 回数えてしまいます。加算を 1 に留めることで重複を防ぎます。
Q3. n が奇数のとき偶数約数がゼロになるのはなぜ?
d|n かつ 2|d と仮定すると 2|n が言えます。これは n が奇数という仮定に矛盾します。
Q4. 非常に大きな n でも動作するか?
Python の int は任意精度整数なので桁あふれはありません。n=10¹² でも √(n/2)≈7×10⁵ ステップ程度です。