問題
石の山が 2 つあり、個数は $a,b$。2 人が交互に次のどちらかを行う。
- どちらか一方の山から 1 個以上の好きな数を取る
- 両方の山から 同じ数 を 1 個以上取る
石を取れなくなった人の負け。
$Q$ 個の局面 $(a_j,b_j)$ それぞれについて、手番の人の 勝ち手の数 を出力せよ。勝ち手とは、指した後の局面が「手番の人が負ける局面」(負け局面)になる手のこと。負け局面なら 0 を出力する。
手は「どの山から(または両方から)何個取るか」で区別する。$a=b$ のとき、左の山から $t$ 個と右の山から $t$ 個は別の手として数える。
入力形式
Q
a_1 b_1
...
a_Q b_Q
制約
| # | 条件 |
|---|---|
| 1 | $1\le Q\le 2\times10^5$ |
| 2 | $0\le a_j,b_j\le 2\times10^{18}$ |
| 3 | 入力はすべて整数 |
入出力例
入力例1
6
0 0
2 3
5 5
4 9
165580140 267914295
1618033988749894848 2618033988749894848出力例1
0
2
3
1
0
0($(2,3)$: 両方から 1 個取って $(1,2)$、大きい山を 1 個にして $(2,1)$ の 2 手。$(5,5)$: 左を 3 にする・右を 3 にする・両方から 5 個取る の 3 手。$(4,9)$: 9 を 7 にする 1 手。5 行目は $k=102334155$(フィボナッチ数 $F_{40}$)の負け局面で、int(k * phi) で判定すると 165580141 と 1 ずれて「勝ち局面」と誤る。6 行目は $k=10^{18}$ の負け局面)
Wythoff のゲームは W. A. Wythoff「A modification of the game of Nim」(Nieuw Archief voor Wiskunde 7, 1907)で解かれた。負け局面の小さい方の列は OEIS A000201(下 Wythoff 数列)、大きい方は A001950。本セッションでは原文を参照しておらず、負け局面の式は $200\times200$ の全局面探索と照合して確かめた。
概念図:負け局面の配置
ヒント(クリックで段階的に開示)
ヒント1(方向性)
$201\times201$ 程度なら「その局面から負け局面へ行けるか」の DP で全局面を塗れる。負け局面を小さい順に並べると $(0,0),(1,2),(3,5),(4,7),(6,10),(8,13),\dots$。差が $0,1,2,3,\dots$ と 1 ずつ増え、小さい方は「まだ出てきていない最小の数」になっている。
ヒント2(アプローチ)
$k$ 番目の負け局面は $(a_k,b_k)=(\lfloor k\varphi\rfloor,\ \lfloor k\varphi\rfloor+k)$、$\varphi=(1+\sqrt5)/2$。すべての正整数は $a_k$ か $b_k$ のどちらか一方にちょうど 1 回だけ現れる(Beatty の定理)。したがって、どの数 $x$ にも「負け局面での相方」$\mathrm{partner}(x)$ が 1 つだけ決まる。
ヒント3(誘導)
$a\le b$、$d=b-a$ とする。勝ち手の候補は 3 種類で、種類ごとに行き先は 1 つ:
- 大きい山を $\mathrm{partner}(a)$ にする($\mathrm{partner}(a)<b$ のとき)
- 小さい山を $\mathrm{partner}(b)$ にする($\mathrm{partner}(b)<a$ のとき)
- 両方から取って差 $d$ の負け局面 $(a_d,a_d+d)$ にする($a_d<a$ のとき)
$\lfloor k\varphi\rfloor=\lfloor (k+\sqrt{5k^2})/2\rfloor$ は (k + isqrt(5*k*k)) // 2 で正確に計算できる。
模範解答 (Python)
模範解答を表示する(まず自力で解いてから)
import sys
from math import isqrt
def a_of(k):
"""下 Wythoff 数 floor(k*phi)。isqrt は整数平方根なので誤差が出ない。"""
return (k + isqrt(5 * k * k)) // 2
def partner(x):
"""x を含む負け局面 (a_k, b_k) の相方。x = 0 なら 0。"""
if x == 0:
return 0
k = (isqrt(5 * x * x) - x) // 2 # x = a_k なら k ≈ x/phi
for kk in (k - 1, k, k + 1, k + 2):
if kk >= 1 and a_of(kk) == x:
return x + kk # b_k = a_k + k
k = (3 * x - isqrt(5 * x * x)) // 2 # x = b_k なら k ≈ x/phi^2
for kk in (k - 1, k, k + 1, k + 2):
if kk >= 1 and a_of(kk) + kk == x:
return x - kk
raise AssertionError(x) # Beatty の定理によりここには来ない
def winning_moves(a, b):
if a > b:
a, b = b, a
d = b - a
ad = a_of(d)
if ad == a:
return 0 # 負け局面
# 勝ち手は 3 種類で、種類ごとに行き先は 1 つ。a == b のときは山を区別して数える
c = 0
if partner(a) < b:
c += 1 # 大きい山を partner(a) に
if partner(b) < a:
c += 1 # 小さい山を partner(b) に
if ad < a:
c += 1 # 両方から a - ad 個ずつ
return c
def main():
data = sys.stdin.buffer.read().split()
q = int(data[0])
out = []
for i in range(q):
a = int(data[1 + 2 * i])
b = int(data[2 + 2 * i])
out.append(str(winning_moves(a, b)))
print("\n".join(out))
main()
計算量: 1 クエリあたり isqrt を定数回($O(\log^2 V)$ ビット演算、$V=2\times10^{18}$ で実質定数)。全体 $O(Q)$。実測で $Q=2\times10^5$、値 $\le10^{18}$ の乱数入力に user 1.56 s。
Step-by-Step 解説
局面 $(a,b)$ が負け ⇔ そこから行けるどの局面も勝ち。$a,b\le200$ の全 40,401 局面を DP で塗ると、負け局面は 153 個($(0,0)$ と、$k=1..76$ の $(a_k,b_k)$ とその左右反転)。この DP は $O(V^3)$ なので $10^{18}$ には使えない。
差 $d=b-a$ ごとに負け局面はちょうど 1 つ(同じ差の局面同士は「両方から取る」手で行き来できるので、2 つあると矛盾する)。さらに同じ数を 2 つの負け局面が共有すると、片方の山から取る手で行き来できてしまう。よって「各差に 1 つ・各数は 1 回だけ」を満たすように小さい順に決まり、それが $(\lfloor k\varphi\rfloor,\lfloor k\varphi^2\rfloor)$ になる($1/\varphi+1/\varphi^2=1$ なので Beatty の定理で 2 列が正整数を分割する)。
int(k * phi) と正確な値を比べた。最初に食い違う $k$ は 102,334,155(フィボナッチ数 $F_{40}$)。$F_n\varphi$ は $F_{n+1}$ に非常に近い(誤差 $\approx\varphi^{-n}$)ので、double の丸めで整数をまたぐ。無作為に選んだ $k$ での誤り率は $k\sim10^{13}$ で 27/20000、$10^{15}$ で 2082/20000、$10^{18}$ で 19819/20000。
正確にするには $\lfloor k\varphi\rfloor=\lfloor(k+\sqrt{5k^2})/2\rfloor$ とし、$\lfloor\sqrt{5k^2}\rfloor$ を math.isqrt で求める($\sqrt{5k^2}$ は無理数なので、整数部分だけで床関数が決まる)。
$x=a_k$ なら $k\approx x/\varphi=x(\sqrt5-1)/2$、$x=b_k$ なら $k\approx x/\varphi^2=x(3-\sqrt5)/2$。どちらも isqrt で近似値を出し、前後数個の $k$ を実際に確かめる。どちらか一方でだけ見つかる(Beatty の定理)。
- 大きい山 $b$ から取る → 行き先 $(a,x)$ が負け局面になる $x$ は $\mathrm{partner}(a)$ ただ 1 つ。$x<b$ のときだけ指せる
- 小さい山 $a$ から取る → $x=\mathrm{partner}(b)<a$ のとき
- 両方から取る → 差は $d$ のまま。差 $d$ の負け局面は $(a_d,a_d+d)$ だけなので、$a_d<a$ のとき
3 つは別の手(取る山が違う)なので重複はない。行き先の局面が同じでも手としては別($(2,3)$ で「両方から 1」と「右を 1 に」はどちらも $\{1,2\}$ に行くが 2 手)。最初の実装はここを集合で重複除去して 40,401 局面中 186 局面で DP と食い違った。
迷った他案と採らなかった理由
| 案 | 計算量 | 採否 | 理由 |
|---|---|---|---|
| 全局面 DP | $O(V^3)$ | 検算用のみ | $V=200$ で数秒。$10^{18}$ は不可能 |
| 負け局面を小さい順に生成して表にする(mex で $a_k$ を決める) | $O(V)$ | 不採用 | 閉じた式を知らなくても作れるが、$V=10^{18}$ では表が作れない |
int(k * phi)(double) | $O(1)$ | 不採用 | $k=F_{40}\approx10^8$ で最初に誤り、$10^{15}$ で 1 割誤る(実測) |
decimal で 50 桁の $\varphi$ | $O(1)$ | 不採用 | 精度を足せば当面は合うが「何桁あれば十分か」を別途示す必要がある。isqrt は常に正確 |
| Zeckendorf 表現(フィボナッチ進数)で判定 | $O(\log V)$ | 不採用 | 「$n$ が $a_k$ の形 ⇔ $n$ の Zeckendorf 表現($F_2=1,F_3=2,\dots$ を基数とする)の末尾の 0 が偶数個」という特徴づけがある($n<5000$ で実測一致)。正しいが、相方の計算まで含めると isqrt 版の方が短い |
| isqrt による整数だけの閉じた式(本解) | $O(1)$ | 採用 | 誤差ゼロ。Python の多倍長整数で $2\times10^{18}$ でもそのまま動く |
前提が変わったら: C++ で書くなら 5*k*k が 64 ビットを超える($k\sim10^{18}$ で $5\times10^{36}$)ので、__int128 と自作の整数平方根が要る。$k\le 4\times10^8$ 程度なら long double(80 ビット)で足りる場合があるが、境界は環境に依存するので本解のように整数で書くのが安全。
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
int(k * (1 + 5 ** 0.5) / 2) | double は 53 ビット。$F_{40}$ で最初に誤る | (k + isqrt(5 * k * k)) // 2 |
| 行き先の局面の集合で重複除去する | 別の山から取る手は別の手 | 3 種類をそれぞれ独立に数える |
| $a=b$ のとき大きい山・小さい山を 1 手にまとめる | 山は区別される | partner(a) < b と partner(b) < a を両方数える(どちらも真) |
| $\mathrm{partner}(0)$ を特別扱いしない | $0$ は $a_0=b_0=0$ で、Beatty の 2 列には含まれない | $x=0$ なら相方 $0$ を返す($(0,b)$ で $b>0$ なら全部取って勝ち) |
| 「差 $d$ の負け局面」を $(a_d, b_d)$ と別に探す | 差 $d$ の負け局面はちょうど 1 つ(Step 2) | a_of(d) 1 回で決まる |
次のステップ
- 発展1: 3 山に一般化した Wythoff のゲームは閉じた式が知られていない。$V\le60$ で全探索し、負け局面の分布を見る
- 発展2: 「両方から取るとき、取る数の差が $k$ 以下なら可」の一般化(Fraenkel の $a$-Wythoff)で、負け局面が $\lfloor n\alpha\rfloor$ の形になる $\alpha$ を求める
- 扱わなかった範囲: 各局面の Grundy 数(Wythoff のゲームの Grundy 数は複雑で、和のゲームにすると本問の方法は使えない)、misère 版
検証ログ(本セッションで実行した数値)
- 全局面 DP との一致(実測): $0\le a,b\le200$ の 40,401 局面で勝ち手の数が全件一致。分布は 0 手 153/1 手 30,812/2 手 7,200/3 手 2,236
- 初版の誤り(実測): 行き先を集合で重複除去した版は 186 局面で不一致(例 $(2,3)$ で 1、正しくは 2)
- 浮動小数の誤り(実測): 最初の不一致 $k=102{,}334{,}155=F_{40}$(double 165580141、正 165580140)。各桁 20,000 個の乱択で $10^{12}$ 以下 0 件、$10^{13}$ 27 件、$10^{14}$ 210 件、$10^{15}$ 2,082 件、$10^{16}$ 12,858 件、$10^{18}$ 19,819 件
- 入力例1 の出力
0 2 3 1 0 0を実行で確認。5・6 行目を double で判定すると 2 つとも「負け局面でない」と誤る - 性能(実測): $Q=2\times10^5$、$a,b\le10^{18}$ の乱数で user 1.56 s(real 4.76 s はメモリの逼迫した開発機のため)。勝ち手 1/2/3 の件数 152,607/36,299/11,094
- 出所ラベル: 件数・秒数は実測(Python 3.14.7)。負け局面の式と Beatty の定理は文献の結果で、本セッションでは $V\le200$ の範囲でのみ実測照合。Wythoff 1907 と OEIS の原文は本セッションでは参照していない
- 検証スクリプト: スクラッチパッドの
d161/q1.py・q1_test.py