問題
地面(頂点 $0$)と頂点 $1, \dots, N$ からなる 根付き森 がある。頂点 $v$ ($1 \le v \le N$) は辺で親 $p_v$ ($0 \le p_v < v$) とつながっている($p_v=0$ なら地面に直接生えている)。辺 $v$ とは「$v$ と $p_v$ を結ぶ辺」のこととする。
先手と後手が交互に、残っている辺を1本選んで切る。切った結果 地面とつながらなくなった部分はすべて消える。辺を切れなくなった人の負け(正規形)。
- 両者最善を尽くしたとき、先手が勝つなら
First、後手が勝つならSecondを出力せよ。 - さらに、先手が最初に切ると勝てる辺 の番号をすべて昇順に出力せよ(先手必敗なら空行)。
入力形式
N
p_1 p_2 ... p_N
制約
| # | 条件 |
|---|---|
| 1 | $1 \le N \le 2 \times 10^5$ |
| 2 | $0 \le p_v < v$ |
入出力例
入力例1
5
0 1 1 0 4出力例1
First
2 3 5(頂点1の下に2,3、頂点4の下に5。Grundy値は3。辺2・辺3・辺5のどれを切っても残りのGrundy値が0になる)
問1 は AtCoder AGC017 D「Game on Tree」(https://atcoder.jp/contests/agc017/tasks/agc017_d 、2026-09-23 参照)と同じ構造(そちらは木1本・根を地面とみなす)。問2(勝ち手の全列挙を $O(N)$ で)は本シリーズ用に追加した拡張。
概念図:森とGrundy値・勝ち手
ヒント(クリックで段階的に開示)
ヒント1(方向性)
地面に生えた木が複数ある = 独立なゲームの和。Sprague–Grundy の定理で「各木の Grundy 値の XOR」が 0 かどうかで勝敗が決まる。問題は 1本の木の Grundy 値 をどう求めるか。
ヒント2(アプローチ)
Colon Principle(コロン原理): 頂点 $u$ から生えている部分木たち(竹の束)は、その XOR と同じ Grundy 値を持つ 1本の竹 に置き換えてもゲームの値が変わらない。長さ $k$ の竹(一直線の辺 $k$ 本)の Grundy 値は $k$。よって「子 $c$ の部分木に辺 $(u,c)$ を1本足したもの」は Grundy 値 $g(c)+1$ を持ち、
$$g(u) = \bigoplus_{c \in \mathrm{child}(u)} \bigl(g(c) + 1\bigr)$$
ヒント3(誘導)
問2 は全辺について「切った後の $g(0)$」を求めると $O(N \cdot \text{深さ})$ になる。代わりに 根から「この頂点の Grundy 値が何になれば全体が 0 になるか」= need(v) を配る。
- $need(0) = 0$
- 辺 $c$ を切る: 親 $u$ の値が $g(u) \oplus (g(c)+1)$ になる → これが $need(u)$ と等しければ勝ち手
- $c$ より下の辺を切る: $g'(c)+1 = need(u) \oplus g(u) \oplus (g(c)+1)$ が必要 → $need(c) = $ その値 $-1$(その値が $0$ なら $g'(c)+1 \ge 1$ なので不可能)
模範解答 (Python)
模範解答を表示する(まず自力で解いてから)
import sys
def main():
data = sys.stdin.buffer.read().split()
n = int(data[0])
par = [0] + [int(x) for x in data[1:1 + n]]
children = [[] for _ in range(n + 1)]
for v in range(1, n + 1):
children[par[v]].append(v)
# p_v < v なので番号の降順に処理すれば子→親の順になる(再帰不要)
g = [0] * (n + 1)
for v in range(n, 0, -1):
g[par[v]] ^= g[v] + 1
# need[v] : 部分木 v の Grundy 値がこれになれば全体が 0 になる(-1 は不可能)
need = [-1] * (n + 1)
need[0] = 0
win = []
for u in range(0, n + 1): # 番号の昇順 = 親→子の順
if need[u] < 0:
continue
gu = g[u]
for c in children[u]:
contrib = g[c] + 1
if gu ^ contrib == need[u]: # 辺 c を切る
win.append(c)
t = need[u] ^ gu ^ contrib # c より下を切るとき g'(c)+1 が満たすべき値
need[c] = t - 1 if t >= 1 else -1
print("First" if g[0] != 0 else "Second")
print(" ".join(map(str, sorted(win))))
main()
計算量: $O(N)$(ソートを含めて $O(N \log N)$、勝ち手は親→子の順に見つかるので実際にはほぼ整列済み)。
Step-by-Step 解説
長さ $k$ の竹から辺を1本切ると、そこより上は消えるので長さ $0..k-1$ のどれにでもなれる。これは石 $k$ 個の Nim の山と完全に同じで、Grundy 値は $k$。
頂点 $u$ に子の枝が複数ぶら下がっているとき、それらは「$u$ を共有した独立なゲームの和」。Colon Principle(Berlekamp–Conway–Guy『Winning Ways』第7章)は、同じ Grundy 値を持つ枝同士はその場で交換してもゲーム全体の値が変わらない ことを保証する。したがって子の寄与 $g(c)+1$(子の部分木を竹に畳んだ長さ + 辺 $(u,c)$ の1本)の XOR が $u$ の値になる。
- $g(2)=g(3)=g(5)=0$(葉)
- $g(1) = (0+1)\oplus(0+1) = 0$、$g(4) = 0+1 = 1$
- $g(0) = (g(1)+1)\oplus(g(4)+1) = 1 \oplus 2 = 3 \ne 0$ →
First
辺を1本切ると、変わるのは その辺から根までのパス上の Grundy 値だけ。根から見て「この頂点がいくつになってくれれば全体が 0 か」を逆算して配ると、各辺の判定は $O(1)$ になる。入力例1では
- $need(0)=0$。辺1: $3 \oplus 1 = 2 \ne 0$ ✗、辺4: $3 \oplus 2 = 1 \ne 0$ ✗
- $need(1) = (0 \oplus 3 \oplus 1) - 1 = 1$。辺2: $g(1) \oplus 1 = 1 = need(1)$ ✓、辺3 も同様 ✓
- $need(4) = (0 \oplus 3 \oplus 2) - 1 = 0$。辺5: $g(4) \oplus 1 = 0 = need(4)$ ✓
部分木 $c$ の中で何を切っても辺 $(u,c)$ は残るので、寄与は $g'(c)+1 \ge 1$。必要な寄与 $t$ が $0$ なら到達不能。これを見落として $need(c)=-1$ を「値 $-1$」として XOR に使うと、Python では負数の XOR が通ってしまい静かに誤答する。
迷った他案と採らなかった理由
| 案 | 計算量 | 採否 | 理由 |
|---|---|---|---|
| 全状態のゲーム木探索(辺集合のビットマスクでメモ化) | $O(2^N \cdot N^2)$ | 検証用のみ | 正しさの基準として $N \le 11$ で使用。本番制約では不可能 |
| 各辺を切ったあと根まで Grundy 値を再計算 | $O(N \cdot \text{深さ})$ | 不採用 | 1本道(パスグラフ)で $O(N^2)=4\times10^{10}$。ランダム木なら通るが最悪ケースを落とす |
| 全方位木DP(rerooting)で各辺の「切断後の値」を求める | $O(N)$ | 不採用 | 本問で変わるのは根までのパスだけで、根を動かす必要はない。need の一方向伝播の方が単純で誤りにくい |
| need の根からの伝播(本解) | $O(N)$ | 採用 | 1回のボトムアップ+1回のトップダウン。判定は XOR 1回 |
前提が変わったら: 辺に色がある(青・赤= Blue-Red Hackenbush)と Grundy 値ではなく Conway の数(surreal number) で評価する別理論になり、本解はそのままでは使えない。閉路を含むグラフ(一般の Green Hackenbush)では Fusion Principle(閉路を地面側に縮約してループに置き換える)が必要で、本問は森に限定した。
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| $g(u) = \bigoplus g(c)$ と書く(+1 忘れ) | 辺 $(u,c)$ 自体も1本の辺としてゲームに含まれることを忘れる | 子の寄与は $g(c)+1$ |
| $g(u) = 1 + \bigoplus g(c)$ と書く | 「+1」の位置を XOR の外に出す | +1 は 各子ごと、XOR の内側 |
| 深い木で再帰 DFS が RecursionError | $N=2\times10^5$ のパス | $p_v < v$ を使い番号の降順ループ(本解)か、明示スタック |
| need が不可能($t=0$)の場合を $-1$ のまま XOR に使う | 「不可能」を値と混同 | need[u] < 0 なら子孫を全部スキップ |
| 勝ち手を「$g(c)+1 = g(0)$ の辺」とだけ判定 | 地面直下の辺しか見ていない | 深い辺を切ると祖先すべての値が変わる。need 伝播で全辺を判定 |
次のステップ
- 発展1: Fusion Principle を実装して、閉路を含む一般グラフの Green Hackenbush の Grundy 値を求める(二重辺連結成分を縮約 → 各成分の辺数の偶奇でループ数が決まる)
- 発展2: Blue-Red Hackenbush の竹の値(2進小数)を計算し、「Grundy 値が使えないゲーム」がどう評価されるかを体験する
- 扱わなかった範囲: 誤規形(misère、最後に取った人が負け)。Hackenbush の misère 版は Grundy 値だけでは決まらない
検証ログ(本セッションで実行した数値)
- 入力例1: 本解
(g=3, 勝ち手=[2,3,5])、全状態探索(g=3, 勝ち手=[2,3,5])で一致 - ランダムテスト: $N \le 11$ の森(各頂点 15% で地面直結)400 件で、Grundy 値と勝ち手の集合の両方を全状態探索(辺集合ビットマスク+メモ化)と比較 → 不一致 0 件
- 模範解答そのもの(標準入力版・降順ループ版)でも、ランダム 100 件($N\le10$)を全状態探索と比較 → 不一致 0 件
- 性能(実測): $N=2\times10^5$、$p_v \in [v-3, v-1]$ の細長い森で 0.064 秒($g(0)=100010$、勝ち手 1 本)。深さが大きいので「根まで再計算」案では最悪に近い形
- 検証スクリプト: スクラッチパッドの
q2.py(上記は実行結果の転記)