스프라그-그런디 정리 (Sprague-Grundy) 는 모든 impartial combinatorial game (두 플레이어가 같은 행동 가능, 무작위성 없음, 완전 정보) 이 Nim-heap 하나와 동등함을 증명하는 정리. 각 게임 상태는 Grundy number (nimber) 로 표현되며, Grundy number 가 0 이면 패배 상태, 0 이 아니면 승리 상태.
문제 상황과 동기
여러 impartial game 이 동시에 진행될 때 (e.g., Nim 의 여러 더미), 각각을 독립 분석 후 XOR 로 합성.
naive: 전체 상태 공간의 game tree 그리기. 상태 폭발.
SG 이론: 각 부분 게임의 Grundy 수를 XOR. 모든 상태를 Nim 으로 환원.
핵심 통찰: 모든 impartial game 의 상태는 자연수 (Grundy number) 로 완전히 추상화되며, 게임의 합성은 XOR.
compute_grundy(max_state, moves): G[0..max_state] = 0 for s = 1..max_state: reachable = empty set for m in moves: if s >= m: reachable.add(G[s - m]) G[s] = mex(reachable) return Gmex(S): g = 0 while g in S: g++ return g# 게임 합성xor_sum = 0for g in grundy_numbers_of_each_game: xor_sum ^= gwinner = "First" if xor_sum != 0 else "Second"
구현
// Grundy number DP + 게임 합성#include <bits/stdc++.h>using namespace std;int mex(const vector<bool>& seen) { int g = 0; while (g < (int)seen.size() && seen[g]) g++; return g;}int main() { const int MAX = 1000; vector<int> moves = {1, 3, 4}; vector<int> G(MAX + 1, 0); for (int s = 1; s <= MAX; s++) { vector<bool> seen(MAX + 1, false); for (int m : moves) if (s >= m) seen[G[s - m]] = true; G[s] = mex(seen); } int N; cin >> N; if (G[N]) cout << "G(" << N << ") = " << G[N] << " -> First wins\n"; else cout << "G(" << N << ") = 0 -> Second wins\n"; // 여러 게임 합성 int k; cin >> k; int xsum = 0; for (int i = 0; i < k; i++) { int p; cin >> p; // Nim: Grundy = pile size xsum ^= p; } cout << "Nim XOR = " << xsum << " -> " << (xsum ? "First" : "Second") << "\n"; return 0;}
# Grundy number + Nim XORdef mex(seen): g = 0 while g < len(seen) and seen[g]: g += 1 return gMAX = 1000moves = [1, 3, 4]G = [0] * (MAX + 1)for s in range(1, MAX + 1): seen = [False] * (MAX + 1) for m in moves: if s >= m: seen[G[s - m]] = True G[s] = mex(seen)N = int(input())if G[N]: print(f"G({N}) = {G[N]} -> First wins")else: print(f"G({N}) = 0 -> Second wins")k = int(input())piles = list(map(int, input().split()))xsum = 0for p in piles: xsum ^= pprint(f"Nim XOR = {xsum} -> {'First' if xsum else 'Second'}")
import java.util.*;import java.io.*;public class Main { static int mex(boolean[] seen) { int g = 0; while (g < seen.length && seen[g]) g++; return g; } public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); int MAX = 1000; int[] moves = {1, 3, 4}; int[] G = new int[MAX + 1]; for (int s = 1; s <= MAX; s++) { boolean[] seen = new boolean[MAX + 1]; for (int m : moves) if (s >= m) seen[G[s - m]] = true; G[s] = mex(seen); } int N = Integer.parseInt(br.readLine()); if (G[N] != 0) System.out.println("G(" + N + ") = " + G[N] + " -> First wins"); else System.out.println("G(" + N + ") = 0 -> Second wins"); int k = Integer.parseInt(br.readLine()); StringTokenizer st = new StringTokenizer(br.readLine()); int xsum = 0; for (int i = 0; i < k; i++) xsum ^= Integer.parseInt(st.nextToken()); System.out.println("Nim XOR = " + xsum + " -> " + (xsum != 0 ? "First" : "Second")); }}
stdin
531 2 3
결과
G(5) = 3 -> First winsNim XOR = 0 -> Second
stdin
223 3
결과
G(2) = 0 -> Second winsNim XOR = 0 -> Second
복잡도
항목
값
Grundy DP 시간
O(S x M) (S: 상태 수, M: 행동 수)
게임 합성 시간
O(K) (K: 게임 수)
공간
O(S) 또는 O(1)
변형 / 활용
응용 분야
유형
특징
Nim 계열
Grundy = pile size
Take-away 계열
행동 집합에 따라 Grundy 주기적
게임 분할
한 행동이 게임을 두 개로 분할
Green Hackenbush
트리 절단, Grundy = tree height XOR
SG + DP
다각형 게임 (BOJ 13034): N-각형 대각선 긋기. 분할 영역의 Grundy XOR.
💬 댓글