問題
山が $T$ 個あり、$i$ 番目の山には $x_i$ 個の石がある。2 人が交互に、どれか 1 つの山を選び、そこから $s\in S$ 個($S$ は与えられた正整数の集合)を取る。取れる山が無くなった側が負け。先手が必勝なら First、そうでなければ Second を出力せよ。
入力形式
k T
s_1 s_2 ... s_k
x_1 x_2 ... x_T
制約
| # | 条件 |
|---|---|
| 1 | $1\le k\le 6$、$1\le s_i\le 30$(互いに異なる) |
| 2 | $1\le T\le 10^5$ |
| 3 | $0\le x_i\le 10^{18}$ |
| 4 | 入力はすべて整数 |
入出力例
入力例1
3 1
1 2 3
10出力例1
First($S=\{1,2,3\}$ では $n\bmod 4$ が Grundy 数。$10\bmod 4=2\ne0$ なので先手必勝)
入力例2
2 3
2 5
7 10 4出力例2
First($S=\{2,5\}$ の Grundy 数は $g(0..10)=0,0,1,1,0,2,1,0,0,1,1$。$g(7)\oplus g(10)\oplus g(4)=0\oplus1\oplus0=1$)
入力例3
3 2
1 3 4
7 7出力例3
Second(同じ大きさの山が 2 つなので XOR は 0。$g(7)=0$ でもある)
背景と出典
「複数の山の勝敗は各山の Grundy 数の XOR」という Sprague–Grundy の定理と、引き算ゲームの Grundy 数列が($S$ が有限なら)最終的に周期的になることは、組合せゲーム理論の標準事項(Berlekamp–Conway–Guy『Winning Ways』にあると私は記憶している。本セッションでは原典を開いておらず未確認)。
周期が存在する理由は本問の中で示せる(Step 2)。ただし周期の長さの上界は示していない。実測で分かったのは、$k\le6,\ s\le30$ のランダム 500 集合での最大値(検証ログ)だけで、最悪値は未調査。
ヒント(クリックで段階的に開示)
ヒント1(方向性)
石の個数 $n$ の山の Grundy 数 $g(n)$ は $g(n)=\operatorname{mex}\{g(n-s)\mid s\in S,\ s\le n\}$。$x\le10^{18}$ なので表は作れないが、$g(n)$ は直前 $m=\max S$ 個の値だけから決まる。ここから何が言えるか。
ヒント2(アプローチ)
長さ $m$ の窓($g(n-m),\dots,g(n-1)$)を、窓が出てくるたびに辞書に記録する。値は高々 $k$ 以下の整数なので窓の種類は有限で、いつか必ず同じ窓が 2 回現れる。2 回目が来た時点で、そこから先は 1 回目の続きの繰り返し。
ヒント3(誘導)
窓が最初の値 $s_0$(窓の終端が添字 $s-1$)で記録され、$n$ で再び現れたなら、$i\ge s-m$ で $g(i+(n-s))=g(i)$。長い $x$ は g[pre + (x - pre) % per](pre = s - m、per = n - s)で引ける。山ごとに引いた値を XOR する。
---
模範解答 (Python)
模範解答を表示する(まず自力で解いてから)
import sys
def grundy_cycle(S):
m = max(S); g = []; seen = {}
# g[i] for i>=0; window = last m values (padded: values for negative idx never used since moves need a<=n)
n = 0
while True:
used = {g[n-a] for a in S if a <= n}
v = 0
while v in used: v += 1
g.append(v); n += 1
if n >= m:
key = tuple(g[n-m:n])
if key in seen:
s = seen[key] # window ending at index s-1 equals window ending at n-1
return g, s - m, n - s # pre = first index of periodic part? see below
seen[key] = n
def gval(g, pre_end, period, x):
# g[i] == g[i+period] for i >= pre_end
if x < len(g): return g[x]
return g[pre_end + (x - pre_end) % period]
def main():
data = sys.stdin.read().split(); k, t = int(data[0]), int(data[1])
S = sorted(map(int, data[2:2+k])); xs = data[2+k:2+k+t]
g, pre, per = grundy_cycle(S)
out = []
for x in xs:
out.append(gval(g, pre, per, int(x)))
# XOR
r = 0
for v in out: r ^= v
print("First" if r else "Second")
main()
計算量: 表の作成 $O(L\cdot k)$($L=\text{pre}+\text{per}$ が周期検出までの長さ)、クエリ $O(T)$。実測(Python 3.14.7、M1 Mac)で $S=\{7,13,18,26,29,30\}$(pre=200、per=56)、$T=10^5$ 個の $x\in[10^{17},10^{18}]$ が 0.07 s(プロセス起動込み)。
---
Step-by-Step 解説
$g(n)=\operatorname{mex}\{g(n-s)\}$ で、使うのは $g(n-1),\dots,g(n-m)$ だけ。さらに $n\ge m$ なら全ての $s\le m$ が引ける($s\le n$ の条件が常に真)ので、$n\ge m$ の範囲では $g(n)$ は窓の値だけで決まる関数になる。$n<m$ の部分は窓が満たされていないので、窓の記録は $n\ge m$ から始める(コードの if n >= m)。
値の種類が高々 $k+1$ 通りなので窓の種類は有限。同じ窓が 2 回現れたら、続きも同じ。これが周期の存在の証明で、同時に検出方法そのものになる(最初の繰り返しを見つけた所で打ち切る)。
窓 $g[n-m..n-1]$ を記録した時の n を辞書に入れる。同じ窓が n で再び出たとき、前回の記録を s とすると $g[s-m..s-1]=g[n-m..n-1]$、したがって $i\ge s-m$ で $g[i]=g[i+(n-s)]$。pre = s - m、per = n - s。表 g の長さは n なのでその範囲は直接引き、外は剰余で引く。
Sprague–Grundy の定理で、各山の Grundy 数の XOR が 0 なら後手必勝。First / Second を出す。
$S=\{1,2,\dots,m\}$ のときだけ $g(n)=n\bmod(m+1)$。それ以外の $S$ に使うと外れる(ランダム 500 集合中 499 集合で 3000 項のどこかが不一致)。
---
迷った他案と採らなかった理由
| 案 | 計算量 | 採否 | 理由 |
|---|---|---|---|
| 窓の辞書による周期検出(本解) | $O(L k)$ | 採用 | 繰り返しが「窓が同じ」から導けて、証明と実装が同じ形 |
| $n\bmod(m+1)$、$n\bmod(\sum S)$ などの公式を当てる | $O(1)$ | 不採用 | $S=\{1..m\}$ 以外で外れる(499/500)。公式が成り立つ $S$ の特徴づけは扱わない |
| $x$ ごとに $\le10^{18}$ まで DP | $O(x)$ | 不採用 | 表が作れない |
| 周期を $(k+1)^m$ の上界で先に決めて表を作る | $O((k+1)^m)$ | 不採用 | $k=6,m=30$ では桁外れ。実測の周期は最大 376(検証ログ)なので、検出で止める方が桁違いに短い |
前提が変わったら:
- $k$ が大きい・$s$ が大きい($m\ge 40$ 程度)と、窓の種類が増えて周期が長くなりうる。周期が表に収まるかを先に実測してから使う
- 取り方が山ごとに違う(山 $i$ だけ集合 $S_i$)なら、集合ごとに表を作って XOR する形に拡張できる。本問は扱わない
- 最後に取った人が負け(misère)なら XOR 判定がそのまま使えない(Sprague–Grundy の対象外)
---
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
全ての $S$ に x % (max(S)+1) を使う | $\{1..m\}$ の結果を一般化する | 窓を辞書で検出する(誤答 499/500 集合) |
| 窓を $n<m$ から記録する | 範囲外参照を避けるつもりで早く始める | $n\ge m$ から(Step 1) |
pre を s にしてしまう | 窓の終端と先頭を取り違える | 窓の先頭 $s-m$ が繰り返しの先頭 |
| 山ごとの Grundy 数を足す/最大を取る | XOR を使うのを忘れる | r ^= v |
次のステップ
発展1: 周期の長さが最大になる $S$ を探索で見つける($k\le4,\ s\le20$ なら全探索できる)
- 発展2: $S$ を「全ての正整数」から $S=\{1,\ldots\}$ に加え「素数個」に変える(無限集合では周期的とは限らない)
- 次回予告: 焦点帯外(M2・M3)の問題
検証ログ(本セッションで実行した数値)
- 全探索との照合(実測): $k\le6,\ s\le30$ のランダム 500 集合で、3000 項の表(定義どおりの mex)と周期での値が全項一致。そのうち 3 山の入力を実際にプログラムへ流した結果(First/Second)も 500 件全件一致
- 周期の大きさ(実測): 500 集合での最大周期 376($S=\{8,21,29\}$)、最大 pre は 271($S=\{2,12,17,19,28,30\}$)。周期の上界は未証明
- 誤った方法(実測): $n \bmod (\max S+1)$ は 500 集合中 499 集合で表と不一致
- 入力例(実測): 例 1 First、例 2 First、例 3 Second(出力例どおり)。例 2 の $g(0..10)$ は表の出力
[0,0,1,1,0,2,1,0,0,1,1]と一致 - 実行時間(実測): 上の $T=10^5$ で 0.07 s
- 扱っていない範囲: 周期長の上界、misère 版、山ごとに異なる $S$、『Winning Ways』の原典確認