問題
$N$ 頂点の DAG $G$ が与えられる。各頂点の Grundy 値 $G(v) = \text{mex}(\{G(w) \mid v \to w\})$ を計算し、$Q$ クエリ $(l, r)$ に対してトポロジカル順で $l$ 番目から $r$ 番目の頂点の Grundy 値の XOR 総和を答えよ。
制約
$1 \le N \le 5 \times 10^4$
$0 \le M \le 2 \times 10^5$
$1 \le Q \le 10^5$
DAG 保証
時間制限: 2sec / メモリ: 256MB
入出力例
入力例 1
5 5
1 2
1 3
2 4
3 4
4 5
3
1 3
2 5
1 5
出力例 1
3
2
1
概念図: Grundy値の計算フロー
ヒント(段階的開示)
ヒント1: 方向性
トポロジカルソートの逆順(葉から根へ)に mex を計算すると Grundy 値が求まる。その後は prefix XOR で区間 XOR クエリを O(1) で処理できる。
ヒント2: mex の計算
mex(S) は集合 S に含まれない最小の非負整数。出次数 0 の頂点の Grundy 値は 0。各頂点は後継の Grundy 値集合の mex を取る。N が大きいので mex の計算には set を使う。
ヒント3: 実装骨格
# トポロジカルソート後、逆順処理
for v in reversed(topo):
s = {grundy[w] for w in graph[v]}
mx = 0
while mx in s: mx += 1
grundy[v] = mx
# prefix XOR
prefix = [0] * (N + 1)
for i, v in enumerate(topo):
prefix[i+1] = prefix[i] ^ grundy[v]
# クエリ l..r (1-indexed)
ans = prefix[r] ^ prefix[l-1]
模範解答 (Python)
import sys
from collections import deque
input = sys.stdin.readline
def solve():
N, M = map(int, input().split())
graph = [[] for _ in range(N+1)]
in_deg = [0] * (N+1)
for _ in range(M):
u, v = map(int, input().split())
graph[u].append(v)
in_deg[v] += 1
# トポロジカルソート (Kahn's algorithm)
dq = deque(i for i in range(1, N+1) if in_deg[i] == 0)
topo = []
while dq:
v = dq.popleft()
topo.append(v)
for w in graph[v]:
in_deg[w] -= 1
if in_deg[w] == 0:
dq.append(w)
# Grundy値の計算(逆順)
grundy = [0] * (N+1)
for v in reversed(topo):
s = {grundy[w] for w in graph[v]}
mx = 0
while mx in s:
mx += 1
grundy[v] = mx
# prefix XOR(トポロジカル順インデックス)
prefix = [0] * (N+1)
for i, v in enumerate(topo):
prefix[i+1] = prefix[i] ^ grundy[v]
Q = int(input())
out = []
for _ in range(Q):
l, r = map(int, input().split())
out.append(prefix[r] ^ prefix[l-1])
print('\n'.join(map(str, out)))
solve()
Step-by-Step 解説
1Grundy 値の定義
Sprague-Grundy 定理: DAG ゲームの各頂点 $v$ の Grundy 値 $G(v) = \text{mex}(\{G(w) \mid v \to w \in E\})$。出次数 0 の頂点は $G(v) = 0$(敗北点)。
Sprague-Grundy 定理: DAG ゲームの各頂点 $v$ の Grundy 値 $G(v) = \text{mex}(\{G(w) \mid v \to w \in E\})$。出次数 0 の頂点は $G(v) = 0$(敗北点)。
2トポロジカルソート
Kahn's Algorithm で入次数 0 の頂点から BFS。逆順処理で葉 → 根の順に mex を計算。
Kahn's Algorithm で入次数 0 の頂点から BFS。逆順処理で葉 → 根の順に mex を計算。
3mex の計算
各頂点の後継 Grundy 値を set に収集し、0 から順に含まれない最小値を探す。最大 mex 値は出次数以下なので O(deg(v)) で計算。
各頂点の後継 Grundy 値を set に収集し、0 から順に含まれない最小値を探す。最大 mex 値は出次数以下なので O(deg(v)) で計算。
4prefix XOR
トポロジカル順に並べた Grundy 値列に対して prefix XOR 配列を構築。区間 [l,r] の XOR は
トポロジカル順に並べた Grundy 値列に対して prefix XOR 配列を構築。区間 [l,r] の XOR は
prefix[r] ^ prefix[l-1] で O(1)。
5計算量
全体: $O(N + M + Q)$。mex 計算は全辺で O(M) 合計。
全体: $O(N + M + Q)$。mex 計算は全辺で O(M) 合計。
計算量
トポロジカルソート: $O(N + M)$
全 mex 計算: $O(N + M)$(全辺 O(M) で set 構築)
prefix XOR 構築: $O(N)$
クエリ: $O(1)$ per query
合計: $O(N + M + Q)$
全 mex 計算: $O(N + M)$(全辺 O(M) で set 構築)
prefix XOR 構築: $O(N)$
クエリ: $O(1)$ per query
合計: $O(N + M + Q)$
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| mex 計算で無限ループ | set に基づく判定 | while mx in s: mx += 1 |
| トポロジカル順と逆順を混同 | Grundy 値は逆順(葉→根)で計算 | for v in reversed(topo) |
| prefix XOR の 0-indexed/1-indexed ずれ | クエリで l-1 を忘れ | prefix[r] ^ prefix[l-1] |
| 出次数 0 でも mex 計算 | 空集合の mex = 0 | 空集合チェック不要(mx=0 が正解) |
次のステップ
- 発展: 複数のゲームを同時プレイ(XOR で Grundy 値を結合)
- 応用: Misère Nim への変換(Grundy 値が全 0 でない場合の勝利条件変更)
- 類題: DAG ゲームの先手必勝条件と最適手の復元